ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK'\l8l renderer.phpnu[. /** * Renderer for core_grading subsystem * * @package core_grading * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Standard HTML output renderer for core_grading subsystem * * @package core_grading * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @category grading */ class core_grading_renderer extends plugin_renderer_base { /** * Renders the active method selector at the grading method management screen * * @param grading_manager $manager * @param moodle_url $targeturl * @return string */ public function management_method_selector(grading_manager $manager, moodle_url $targeturl) { $method = $manager->get_active_method(); $methods = $manager->get_available_methods(false); $methods['none'] = get_string('gradingmethodnone', 'core_grading'); $selector = new single_select(new moodle_url($targeturl, array('sesskey' => sesskey())), 'setmethod', $methods, empty($method) ? 'none' : $method, null, 'activemethodselector'); $selector->set_label(get_string('changeactivemethod', 'core_grading')); $selector->set_help_icon('gradingmethod', 'core_grading'); return $this->output->render($selector); } /** * Renders an action icon at the gradng method management screen * * @param moodle_url $url action URL * @param string $text action text * @param string $icon the name of the icon to use * @return string */ public function management_action_icon(moodle_url $url, $text, $icon) { $img = $this->output->pix_icon($icon, ''); $txt = html_writer::tag('div', $text, array('class' => 'action-text')); return html_writer::link($url, $img . $txt, array('class' => 'action btn btn-lg')); } /** * Renders a message for the user, typically as an action result * * @param string $message * @return string */ public function management_message($message) { $this->page->requires->strings_for_js(array('clicktoclose'), 'core_grading'); $this->page->requires->yui_module('moodle-core_grading-manage', 'M.core_grading.init_manage'); return $this->output->box(format_string($message) . ' - ' . html_writer::tag('span', ''), 'message', 'actionresultmessagebox'); } /** * Renders the template action icon * * @param moodle_url $url action URL * @param string $text action text * @param string $icon the name of the icon to use * @param string $class extra class of this action * @return string */ public function pick_action_icon(moodle_url $url, $text, $icon = '', $class = '') { $img = $this->output->pix_icon($icon, ''); $txt = html_writer::tag('div', $text, array('class' => 'action-text')); return html_writer::link($url, $img . $txt, array('class' => 'action '.$class)); } } PK'\) form/lib.phpnu[. /** * Common classes used by gradingform plugintypes are defined here * * @package core_grading * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Class represents a grading form definition used in a particular area * * General data about definition is stored in the standard DB table * grading_definitions. A separate entry is created for each grading area * (i.e. for each module). Plugins may define and use additional tables * to store additional data about definitions. * * Advanced grading plugins must declare a class gradingform_xxxx_controller * extending this class and put it in lib.php in the plugin folder. * * See {@link gradingform_rubric_controller} as an example * * Except for overwriting abstract functions, plugin developers may want * to overwrite functions responsible for loading and saving of the * definition to include additional data stored. * * @package core_grading * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @category grading */ abstract class gradingform_controller { /** undefined definition status */ const DEFINITION_STATUS_NULL = 0; /** the form is currently being edited and is not ready for usage yet */ const DEFINITION_STATUS_DRAFT = 10; /** the form was marked as ready for actual usage */ const DEFINITION_STATUS_READY = 20; /** @var stdClass the context */ protected $context; /** @var string the frankenstyle name of the component */ protected $component; /** @var string the name of the gradable area */ protected $area; /** @var int the id of the gradable area record */ protected $areaid; /** @var stdClass|false the definition structure */ protected $definition = false; /** @var array graderange array of valid grades for this area. Use set_grade_range and get_grade_range to access this */ private $graderange = null; /** @var bool if decimal values are allowed as grades. */ private $allowgradedecimals = false; /** @var boolean|null cached result of function has_active_instances() */ protected $hasactiveinstances = null; /** * Do not instantinate this directly, use {@link grading_manager::get_controller()} * * @param stdClass $context the context of the form * @param string $component the frankenstyle name of the component * @param string $area the name of the gradable area * @param int $areaid the id of the gradable area record */ public function __construct(stdClass $context, $component, $area, $areaid) { global $DB; $this->context = $context; list($type, $name) = core_component::normalize_component($component); $this->component = $type.'_'.$name; $this->area = $area; $this->areaid = $areaid; $this->load_definition(); } /** * Returns controller context * * @return stdClass controller context */ public function get_context() { return $this->context; } /** * Returns gradable component name * * @return string gradable component name */ public function get_component() { return $this->component; } /** * Returns gradable area name * * @return string gradable area name */ public function get_area() { return $this->area; } /** * Returns gradable area id * * @return int gradable area id */ public function get_areaid() { return $this->areaid; } /** * Is the form definition record available? * * Note that this actually checks whether the process of defining the form ever started * and not whether the form definition should be considered as final. * * @return boolean */ public function is_form_defined() { return ($this->definition !== false); } /** * Is the grading form defined and ready for usage? * * @return boolean */ public function is_form_available() { return ($this->is_form_defined() && $this->definition->status == self::DEFINITION_STATUS_READY); } /** * Is the grading form saved as a shared public template? * * @return boolean */ public function is_shared_template() { return ($this->get_context()->id == context_system::instance()->id and $this->get_component() == 'core_grading'); } /** * Is the grading form owned by the given user? * * The form owner is the user who created this instance of the form. * * @param int $userid the user id to check, defaults to the current user * @return boolean|null null if the form not defined yet, boolean otherwise */ public function is_own_form($userid = null) { global $USER; if (!$this->is_form_defined()) { return null; } if (is_null($userid)) { $userid = $USER->id; } return ($this->definition->usercreated == $userid); } /** * Returns a message why this form is unavailable. Maybe overriden by plugins to give more details. * @see is_form_available() * * @return string */ public function form_unavailable_notification() { if ($this->is_form_available()) { return null; } return get_string('gradingformunavailable', 'grading'); } /** * Returns URL of a page where the grading form can be defined and edited. * * @param moodle_url $returnurl optional URL of a page where the user should be sent once they are finished with editing * @return moodle_url */ public function get_editor_url(?moodle_url $returnurl = null) { $params = array('areaid' => $this->areaid); if (!is_null($returnurl)) { $params['returnurl'] = $returnurl->out(false); } return new moodle_url('/grade/grading/form/'.$this->get_method_name().'/edit.php', $params); } /** * Extends the module settings navigation * * This function is called when the context for the page is an activity module with the * FEATURE_ADVANCED_GRADING, the user has the permission moodle/grade:managegradingforms * and there is an area with the active grading method set to the given plugin. * * @param settings_navigation $settingsnav {@link settings_navigation} * @param navigation_node $node {@link navigation_node} */ public function extend_settings_navigation(settings_navigation $settingsnav, ?navigation_node $node=null) { // do not extend by default } /** * Extends the module navigation * * This function is called when the context for the page is an activity module with the * FEATURE_ADVANCED_GRADING and there is an area with the active grading method set to the given plugin. * * @param global_navigation $navigation {@link global_navigation} * @param navigation_node $node {@link navigation_node} */ public function extend_navigation(global_navigation $navigation, ?navigation_node $node=null) { // do not extend by default } /** * Returns the grading form definition structure * * @param boolean $force whether to force loading from DB even if it was already loaded * @return stdClass|false definition data or false if the form is not defined yet */ public function get_definition($force = false) { if ($this->definition === false || $force) { $this->load_definition(); } return $this->definition; } /** * Returns the form definition suitable for cloning into another area * * @param gradingform_controller $target the controller of the new copy * @return stdClass definition structure to pass to the target's {@link update_definition()} */ public function get_definition_copy(gradingform_controller $target) { if (get_class($this) != get_class($target)) { throw new coding_exception('The source and copy controller mismatch'); } if ($target->is_form_defined()) { throw new coding_exception('The target controller already contains a form definition'); } $old = $this->get_definition(); // keep our id $new = new stdClass(); $new->copiedfromid = $old->id; $new->name = $old->name; // once we support files embedded into the description, we will want to // relink them into the new file area here (that is why we accept $target) $new->description = $old->description; $new->descriptionformat = $old->descriptionformat; $new->options = $old->options; $new->status = $old->status; return $new; } /** * Saves the defintion data into the database * * The implementation in this base class stores the common data into the record * into the {grading_definition} table. The plugins are likely to extend this * and save their data into own tables, too. * * @param stdClass $definition data containing values for the {grading_definition} table * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user */ public function update_definition(stdClass $definition, $usermodified = null) { global $DB, $USER; if (is_null($usermodified)) { $usermodified = $USER->id; } if (!empty($this->definition->id)) { // prepare a record to be updated $record = new stdClass(); // populate it with scalar values from the passed definition structure foreach ($definition as $prop => $val) { if (is_array($val) or is_object($val)) { // probably plugin's data continue; } $record->{$prop} = $val; } // make sure we do not override some crucial values by accident if (!empty($record->id) and $record->id != $this->definition->id) { throw new coding_exception('Attempting to update other definition record.'); } $record->id = $this->definition->id; unset($record->areaid); unset($record->method); unset($record->timecreated); // set the modification flags $record->timemodified = time(); $record->usermodified = $usermodified; $DB->update_record('grading_definitions', $record); } else if ($this->definition === false) { // prepare a record to be inserted $record = new stdClass(); // populate it with scalar values from the passed definition structure foreach ($definition as $prop => $val) { if (is_array($val) or is_object($val)) { // probably plugin's data continue; } $record->{$prop} = $val; } // make sure we do not override some crucial values by accident if (!empty($record->id)) { throw new coding_exception('Attempting to create a new record while there is already one existing.'); } unset($record->id); $record->areaid = $this->areaid; $record->method = $this->get_method_name(); $record->timecreated = time(); $record->usercreated = $usermodified; $record->timemodified = $record->timecreated; $record->usermodified = $record->usercreated; if (empty($record->status)) { $record->status = self::DEFINITION_STATUS_DRAFT; } if (empty($record->descriptionformat)) { $record->descriptionformat = FORMAT_MOODLE; // field can not be empty } $DB->insert_record('grading_definitions', $record); } else { throw new coding_exception('Unknown status of the cached definition record.'); } } /** * Formats the definition description for display on page * * @return string */ public function get_formatted_description() { if (!isset($this->definition->description)) { return ''; } return format_text($this->definition->description, $this->definition->descriptionformat); } /** * Returns the current instance (either with status ACTIVE or NEEDUPDATE) for this definition for the * specified $raterid and $itemid (if multiple raters are allowed, or only for $itemid otherwise). * * @param int $raterid * @param int $itemid * @param boolean $idonly * @return mixed if $idonly=true returns id of the found instance, otherwise returns the instance object */ public function get_current_instance($raterid, $itemid, $idonly = false) { global $DB; $params = array( 'definitionid' => $this->definition->id, 'itemid' => $itemid, 'status1' => gradingform_instance::INSTANCE_STATUS_ACTIVE, 'status2' => gradingform_instance::INSTANCE_STATUS_NEEDUPDATE); $select = 'definitionid=:definitionid and itemid=:itemid and (status=:status1 or status=:status2)'; if (false) { // TODO MDL-31237 should be: if ($manager->allow_multiple_raters()) $select .= ' and raterid=:raterid'; $params['raterid'] = $raterid; } if ($idonly) { if ($current = $DB->get_record_select('grading_instances', $select, $params, 'id', IGNORE_MISSING)) { return $current->id; } } else { if ($current = $DB->get_record_select('grading_instances', $select, $params, '*', IGNORE_MISSING)) { return $this->get_instance($current); } } return null; } /** * Returns list of ACTIVE instances for the specified $itemid * (intentionally does not return instances with status NEEDUPDATE) * * @param int $itemid * @return array of gradingform_instance objects */ public function get_active_instances($itemid) { global $DB; $conditions = array('definitionid' => $this->definition->id, 'itemid' => $itemid, 'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE); $records = $DB->get_recordset('grading_instances', $conditions); $rv = array(); foreach ($records as $record) { $rv[] = $this->get_instance($record); } $records->close(); return $rv; } /** * Returns an array of all active instances for this definition. * (intentionally does not return instances with status NEEDUPDATE) * * @param int since only return instances with timemodified >= since * @return array of gradingform_instance objects */ public function get_all_active_instances($since = 0) { global $DB; $conditions = array ($this->definition->id, gradingform_instance::INSTANCE_STATUS_ACTIVE, $since); $where = "definitionid = ? AND status = ? AND timemodified >= ?"; $records = $DB->get_records_select('grading_instances', $where, $conditions); $rv = array(); foreach ($records as $record) { $rv[] = $this->get_instance($record); } return $rv; } /** * Returns true if there are already people who has been graded on this definition. * In this case plugins may restrict changes of the grading definition * * @return boolean */ public function has_active_instances() { global $DB; if (empty($this->definition->id)) { return false; } if ($this->hasactiveinstances === null) { $conditions = array('definitionid' => $this->definition->id, 'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE); $this->hasactiveinstances = $DB->record_exists('grading_instances', $conditions); } return $this->hasactiveinstances; } /** * Returns the object of type gradingform_XXX_instance (where XXX is the plugin method name) * * @param mixed $instance id or row from grading_isntances table * @return gradingform_instance */ protected function get_instance($instance) { global $DB; if (is_scalar($instance)) { // instance id is passed as parameter $instance = $DB->get_record('grading_instances', array('id' => $instance), '*', MUST_EXIST); } if ($instance) { $class = 'gradingform_'. $this->get_method_name(). '_instance'; return new $class($this, $instance); } return null; } /** * This function is invoked when user (teacher) starts grading. * It creates and returns copy of the current ACTIVE instance if it exists. If this is the * first grading attempt, a new instance is created. * The status of the returned instance is INCOMPLETE * * @param int $raterid * @param int $itemid * @return gradingform_instance */ public function create_instance($raterid, $itemid = null) { // first find if there is already an active instance for this itemid if ($itemid && $current = $this->get_current_instance($raterid, $itemid)) { return $this->get_instance($current->copy($raterid, $itemid)); } else { $class = 'gradingform_'. $this->get_method_name(). '_instance'; return $this->get_instance($class::create_new($this->definition->id, $raterid, $itemid)); } } /** * If instanceid is specified and grading instance exists and it is created by this rater for * this item, this instance is returned. * Otherwise new instance is created for the specified rater and itemid * * @param int $instanceid * @param int $raterid * @param int $itemid * @return gradingform_instance * @throws dml_exception */ public function get_or_create_instance($instanceid, $raterid, $itemid) { if (!is_numeric($instanceid)) { $instanceid = null; } return $this->fetch_instance($raterid, $itemid, $instanceid); } /** * If an instanceid is specified and grading instance exists and it is created by this rater for * this item, then the instance is returned. * * If instanceid is not known, then null can be passed to fetch the current instance matchign the specified raterid * and itemid. * * If the instanceid is falsey, or no instance was found, then create a new instance for the specified rater and item. * * @param int $raterid * @param int $itemid * @param int $instanceid * @return gradingform_instance * @throws dml_exception */ public function fetch_instance(int $raterid, int $itemid, ?int $instanceid): gradingform_instance { global $DB; $instance = null; if (null === $instanceid) { if ($instance = $this->get_current_instance($raterid, $itemid)) { return $instance; } $instanceid = $instancerecord->id ?? null; } if (!empty($instanceid)) { $instance = $DB->get_record('grading_instances', [ 'id' => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid, ], '*', IGNORE_MISSING); if ($instance) { return $this->get_instance($instance); } } return $this->create_instance($raterid, $itemid); } /** * Returns the HTML code displaying the preview of the grading form * * Plugins are forced to override this. Ideally they should delegate * the task to their own renderer. * * @param moodle_page $page the target page * @return string */ abstract public function render_preview(moodle_page $page); /** * Deletes the form definition and all the associated data * * @see delete_plugin_definition() * @return void */ public function delete_definition() { global $DB; if (!$this->is_form_defined()) { // nothing to do return; } // firstly, let the plugin delete everything from their own tables $this->delete_plugin_definition(); // then, delete all instances left $DB->delete_records('grading_instances', array('definitionid' => $this->definition->id)); // finally, delete the main definition record $DB->delete_records('grading_definitions', array('id' => $this->definition->id)); $this->definition = false; } /** * Prepare the part of the search query to append to the FROM statement * * @param string $gdid the alias of grading_definitions.id column used by the caller * @return string */ public static function sql_search_from_tables($gdid) { return ''; } /** * Prepare the parts of the SQL WHERE statement to search for the given token * * The returned array cosists of the list of SQL comparions and the list of * respective parameters for the comparisons. The returned chunks will be joined * with other conditions using the OR operator. * * @param string $token token to search for * @return array */ public static function sql_search_where($token) { $subsql = array(); $params = array(); return array($subsql, $params); } // ////////////////////////////////////////////////////////////////////////// /** * Loads the form definition if it exists * * The default implementation just tries to load the record from the {grading_definitions} * table. The plugins are likely to override this with a more complex query that loads * all required data at once. */ protected function load_definition() { global $DB; $this->definition = $DB->get_record('grading_definitions', array( 'areaid' => $this->areaid, 'method' => $this->get_method_name()), '*', IGNORE_MISSING); } /** * Deletes all plugin data associated with the given form definiton * * @see delete_definition() */ abstract protected function delete_plugin_definition(); /** * Returns the name of the grading method plugin, eg 'rubric' * * @return string the name of the grading method plugin, eg 'rubric' * @see PARAM_PLUGIN */ protected function get_method_name() { if (preg_match('/^gradingform_([a-z][a-z0-9_]*[a-z0-9])_controller$/', get_class($this), $matches)) { return $matches[1]; } else { throw new coding_exception('Invalid class name'); } } /** * Returns html code to be included in student's feedback. * * @param moodle_page $page * @param int $itemid * @param array $gradinginfo result of function grade_get_grades if plugin want to use some of their info * @param string $defaultcontent default string to be returned if no active grading is found or for some reason can not be shown to a user * @param boolean $cangrade whether current user has capability to grade in this context * @return string */ public function render_grade($page, $itemid, $gradinginfo, $defaultcontent, $cangrade) { return $defaultcontent; } /** * Sets the range of grades used in this area. This is usually either range like 0-100 * or the scale where keys start from 1. * * Typically modules will call it: * $controller->set_grade_range(make_grades_menu($gradingtype), $gradingtype > 0); * Negative $gradingtype means that scale is used and the grade must be rounded * to the nearest int. Positive $gradingtype means that range 0..$gradingtype * is used for the grades and in this case grade does not have to be rounded. * * Sometimes modules always expect grade to be rounded (like mod_assign does). * * @param array $graderange array where first _key_ is the minimum grade and the * last key is the maximum grade. * @param bool $allowgradedecimals if decimal values are allowed as grades. */ final public function set_grade_range(array $graderange, $allowgradedecimals = false) { $this->graderange = $graderange; $this->allowgradedecimals = $allowgradedecimals; } /** * Returns the range of grades used in this area * * @return array */ final public function get_grade_range() { if (empty($this->graderange)) { return array(); } return $this->graderange; } /** * Returns if decimal values are allowed as grades * * @return bool */ final public function get_allow_grade_decimals() { return $this->allowgradedecimals; } /** * Overridden by sub classes that wish to make definition details available to web services. * When not overridden, only definition data common to all grading methods is made available. * When overriding, the return value should be an array containing one or more key/value pairs. * These key/value pairs should match the definition returned by the get_definition() function. * For examples, look at: * $gradingform_rubric_controller->get_external_definition_details() * $gradingform_guide_controller->get_external_definition_details() * @return array An array of one or more key/value pairs containing the external_multiple_structure/s * corresponding to the definition returned by $controller->get_definition() * @since Moodle 2.5 */ public static function get_external_definition_details() { return null; } /** * Overridden by sub classes that wish to make instance filling details available to web services. * When not overridden, only instance filling data common to all grading methods is made available. * When overriding, the return value should be an array containing one or more key/value pairs. * These key/value pairs should match the filling data returned by the get__filling() function * in the gradingform_instance subclass. * For examples, look at: * $gradingform_rubric_controller->get_external_instance_filling_details() * $gradingform_guide_controller->get_external_instance_filling_details() * * @return array An array of one or more key/value pairs containing the external_multiple_structure/s * corresponding to the definition returned by $gradingform__instance->get__filling() * @since Moodle 2.6 */ public static function get_external_instance_filling_details() { return null; } } /** * Class to manage one gradingform instance. * * Gradingform instance is created for each evaluation of a student, using advanced grading. * It is stored as an entry in the DB table gradingform_instance. * * One instance (usually the latest) has the status INSTANCE_STATUS_ACTIVE. Sometimes it may * happen that a teacher wants to change the definition when some students have already been * graded. In this case their instances change status to INSTANCE_STATUS_NEEDUPDATE. * * To support future use of AJAX for background saving of incomplete evaluations the * status INSTANCE_STATUS_INCOMPLETE is introduced. If 'Cancel' is pressed this entry * is deleted. * When grade is updated the previous active instance receives status INSTANCE_STATUS_ACTIVE. * * Advanced grading plugins must declare a class gradingform_xxxx_instance * extending this class and put it in lib.php in the plugin folder. * * The reference to an instance of this class is passed to an advanced grading form element * included in the grading form, so this class must implement functions for rendering * and validation of this form element. See {@link MoodleQuickForm_grading} * * @package core_grading * @copyright 2011 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @category grading */ abstract class gradingform_instance { /** Valid istance status */ const INSTANCE_STATUS_ACTIVE = 1; /** The grade needs to be updated by grader (usually because of changes is grading method) */ const INSTANCE_STATUS_NEEDUPDATE = 2; /** The grader started grading but did clicked neither submit nor cancel */ const INSTANCE_STATUS_INCOMPLETE = 0; /** Grader re-graded the student and this is the status for previous grade stored as history */ const INSTANCE_STATUS_ARCHIVE = 3; /** @var stdClass record from table grading_instances */ protected $data; /** @var gradingform_controller link to the corresponding controller */ protected $controller; /** * Creates an instance * * @param gradingform_controller $controller * @param stdClass $data */ public function __construct($controller, $data) { $this->data = (object)$data; $this->controller = $controller; } /** * Creates a new empty instance in DB and mark its status as INCOMPLETE * * @param int $definitionid * @param int $raterid * @param int $itemid * @return int id of the created instance */ public static function create_new($definitionid, $raterid, $itemid) { global $DB; $instance = new stdClass(); $instance->definitionid = $definitionid; $instance->raterid = $raterid; $instance->itemid = $itemid; $instance->status = self::INSTANCE_STATUS_INCOMPLETE; $instance->timemodified = time(); $instance->feedbackformat = FORMAT_MOODLE; $instanceid = $DB->insert_record('grading_instances', $instance); return $instanceid; } /** * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with * the specified values) * Plugins may want to override this function to copy data from additional tables as well * * @param int $raterid value for raterid in the duplicate * @param int $itemid value for itemid in the duplicate * @return int id of the new instance */ public function copy($raterid, $itemid) { global $DB; $data = (array)$this->data; // Cast to array to make a copy unset($data['id']); $data['raterid'] = $raterid; $data['itemid'] = $itemid; $data['timemodified'] = time(); $data['status'] = self::INSTANCE_STATUS_INCOMPLETE; $instanceid = $DB->insert_record('grading_instances', $data); return $instanceid; } /** * Returns the current (active or needupdate) instance for the same raterid and itemid as this * instance. This function is useful to find the status of the currently modified instance * * @return gradingform_instance */ public function get_current_instance() { if ($this->get_status() == self::INSTANCE_STATUS_ACTIVE || $this->get_status() == self::INSTANCE_STATUS_NEEDUPDATE) { return $this; } return $this->get_controller()->get_current_instance($this->data->raterid, $this->data->itemid); } /** * Returns the controller * * @return gradingform_controller */ public function get_controller() { return $this->controller; } /** * Returns the specified element from object $this->data * * @param string $key * @return mixed */ public function get_data($key) { if (isset($this->data->$key)) { return $this->data->$key; } return null; } /** * Returns instance id * * @return int */ public function get_id() { return $this->get_data('id'); } /** * Returns instance status * * @return int */ public function get_status() { return $this->get_data('status'); } /** * Marks the instance as ACTIVE and current active instance (if exists) as ARCHIVE */ protected function make_active() { global $DB; if ($this->data->status == self::INSTANCE_STATUS_ACTIVE) { // already active return; } if (empty($this->data->itemid)) { throw new coding_exception('You cannot mark active the grading instance without itemid'); } $currentid = $this->get_controller()->get_current_instance($this->data->raterid, $this->data->itemid, true); if ($currentid && $currentid != $this->get_id()) { $DB->update_record('grading_instances', array('id' => $currentid, 'status' => self::INSTANCE_STATUS_ARCHIVE)); } $DB->update_record('grading_instances', array('id' => $this->get_id(), 'status' => self::INSTANCE_STATUS_ACTIVE)); $this->data->status = self::INSTANCE_STATUS_ACTIVE; } /** * Deletes this (INCOMPLETE) instance from database. This function is invoked on cancelling the * grading form and/or during cron cleanup. * Plugins using additional tables must override this method to remove additional data. * Note that if the teacher just closes the window or presses 'Back' button of the browser, * this function is not invoked. */ public function cancel() { global $DB; // TODO MDL-31239 throw exception if status is not INSTANCE_STATUS_INCOMPLETE $DB->delete_records('grading_instances', array('id' => $this->get_id())); } /** * Updates the instance with the data received from grading form. This function may be * called via AJAX when grading is not yet completed, so it does not change the * status of the instance. * * @param array $elementvalue */ public function update($elementvalue) { global $DB; $newdata = new stdClass(); $newdata->id = $this->get_id(); $newdata->timemodified = time(); if (isset($elementvalue['itemid']) && $elementvalue['itemid'] != $this->data->itemid) { $newdata->itemid = $elementvalue['itemid']; } // TODO MDL-31087 also update: rawgrade, feedback, feedbackformat $DB->update_record('grading_instances', $newdata); foreach ($newdata as $key => $value) { $this->data->$key = $value; } } /** * Calculates the grade to be pushed to the gradebook * * Returned grade must be in range $this->get_controller()->get_grade_range() * Plugins must returned grade converted to int unless * $this->get_controller()->get_allow_grade_decimals() is true. * * @return float|int */ abstract public function get_grade(); /** * Determines whether the submitted form was empty. * * @param array $elementvalue value of element submitted from the form * @return boolean true if the form is empty */ public function is_empty_form($elementvalue) { return false; } /** * Removes the attempt from the gradingform_*_fillings table. * This function is not abstract as to not break plugins that might * use advanced grading. * @param array $data the attempt data */ public function clear_attempt($data) { // This function is empty because the way to clear a grade // attempt will be different depending on the grading method. return; } /** * Called when teacher submits the grading form: * updates the instance in DB, marks it as ACTIVE and returns the grade to be pushed to the gradebook. * $itemid must be specified here (it was not required when the instance was * created, because it might not existed in draft) * * @param array $elementvalue * @param int $itemid * @return int the grade on 0-100 scale */ public function submit_and_get_grade($elementvalue, $itemid) { $elementvalue['itemid'] = $itemid; if ($this->is_empty_form($elementvalue)) { $this->clear_attempt($elementvalue); $this->make_active(); return -1; } $this->update($elementvalue); $this->make_active(); return $this->get_grade(); } /** * Returns html for form element of type 'grading'. If there is a form input element * it must have the name $gradingformelement->getName(). * If there are more than one input elements they MUST be elements of array with * name $gradingformelement->getName(). * Example: {NAME}[myelement1], {NAME}[myelement2][sub1], {NAME}[myelement2][sub2], etc. * ( {NAME} is a shortcut for $gradingformelement->getName() ) * After submitting the form the value of $_POST[{NAME}] is passed to the functions * validate_grading_element() and submit_and_get_grade() * * Plugins may use $gradingformelement->getValue() to get the value passed on previous * form submit * * When forming html it is a plugin's responsibility to analyze flags * $gradingformelement->_flagFrozen and $gradingformelement->_persistantFreeze: * * (_flagFrozen == false) => form element is editable * * (_flagFrozen == false && _persistantFreeze == true) => form element is not editable * but all values are passed as hidden elements * * (_flagFrozen == false && _persistantFreeze == false) => form element is not editable * and no values are passed as hidden elements * * Plugins are welcome to use AJAX in the form element. But it is strongly recommended * that the grading only becomes active when teacher presses 'Submit' button (the * method submit_and_get_grade() is invoked) * * Also client-side JS validation may be implemented here * * @see MoodleQuickForm_grading in lib/form/grading.php * * @param moodle_page $page * @param MoodleQuickForm_grading $gradingformelement * @return string */ abstract function render_grading_element($page, $gradingformelement); /** * Server-side validation of the data received from grading form. * * @param mixed $elementvalue is the scalar or array received in $_POST * @return boolean true if the form data is validated and contains no errors */ public function validate_grading_element($elementvalue) { return true; } /** * Returns the error message displayed if validation failed. * If plugin wants to display custom message, the empty string should be returned here * and the custom message should be output in render_grading_element() * * Please note that in assignments grading in 2.2 the grading form is not validated * properly and this message is not being displayed. * * @see validate_grading_element() * @return string */ public function default_validation_error_message() { return ''; } } PK'\.xNCCform/upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /grade/grading/form/* - Advanced grading methods information provided here is intended especially for developers. === 3.10 === * Removed gradingform_provider. * Removed the following deprecated functions: get_gradingform_export_data delete_gradingform_for_context delete_gradingform_for_userid === 3.6 === * The privacy interface gradingform_provider has been deprecated. Please use gradingform_provider_v2 instead. === 2.5.2 === * Grading methods now can return grade with decimals. See API functions gradingform_controller::set_grade_range() and gradingform_controller::get_allow_grade_decimals(), and also examples in gradingform_rubric_instance::get_grade(). PK'\6form/rubric/styles.cssnu[/* .gradingform_rubric.editor[.frozen|.editable] .criteria .criterion[.first][.last][.odd|.even] .controls .moveup [input type=submit] .delete [input type=submit] .movedown [input type=submit] .description .levels td.level[.first][.last][.odd|.even] div.level-wrapper .definition [textarea] .score span [input type=text] .delete [input type=submit] .addlevel [input type=submit] .remark textarea .addcriterion [input type=submit] .options .optionsheading .option.OPTIONNAME .gradingform_rubric[.review][.evaluate[.editable|.frozen]] .criteria .criterion[.first][.last][.odd|.even] .description .levels td.level[.first][.last][.odd|.even] div.level-wrapper div.radio input .definition .score span */ .gradingform_rubric_editform .status { font-weight: normal; text-transform: uppercase; font-size: 60%; padding: 0.25em; border: 1px solid #eee; } .gradingform_rubric_editform .status.ready { background-color: #e7f1c3; border-color: #aea; } .gradingform_rubric_editform .status.draft { background-color: #f3f2aa; border-color: #ee2; } .gradingform_rubric { padding-bottom: 1.5em; } .gradingform_rubric.editor .criterion .controls, .gradingform_rubric .criterion .description, .gradingform_rubric .criterion .levels, .gradingform_rubric.editor .criterion .addlevel, .gradingform_rubric .criterion .remark, .gradingform_rubric .criterion .levels .level { vertical-align: top; } .gradingform_rubric.editor .criterion .controls, .gradingform_rubric .criterion .description, .gradingform_rubric.editor .criterion .addlevel, .gradingform_rubric .criterion .remark, .gradingform_rubric .criterion .levels .level { padding: 3px; } .gradingform_rubric .criteria { height: 100%; display: flex; width: 100%; overflow: auto; } .gradingform_rubric .criterion { border: 1px solid #ddd; overflow: hidden; } .gradingform_rubric .criterion.even td { background: #f0f0f0; } .gradingform_rubric .criterion.odd td { background: white; } .gradingform_rubric .criterion .description { min-width: 150px; font-weight: bold; } .gradingform_rubric .criterion .levels table { width: 100%; height: 100%; } .gradingform_rubric .criterion .levels, .gradingform_rubric .criterion .levels table, .gradingform_rubric .criterion .levels table tbody { padding: 0; margin: 0; } .gradingform_rubric .criterion .levels .level { border-left: 1px solid #ddd; max-width: 150px; } .gradingform_rubric .criterion .levels .level .level-wrapper { position: relative; } .gradingform_rubric .criterion .levels .level.last { border-right: 1px solid #ddd; } .gradingform_rubric .plainvalue.empty { font-style: italic; color: #aaa; } /* Make invisible the buttons 'Move up' for the first criterion and 'Move down' for the last, because those buttons will make no change */ .gradingform_rubric.editor .criterion.first .controls .moveup input, .gradingform_rubric.editor .criterion.last .controls .movedown input { display: none; } /* replace buttons with images */ .gradingform_rubric.editor .delete input, .gradingform_rubric.editor .duplicate input, .gradingform_rubric.editor .moveup input, .gradingform_rubric.editor .movedown input { background: none 2px 4px / 16px 16px no-repeat; text-indent: -1000em; cursor: pointer; border: none; } .gradingform_rubric.editor .criterion .controls .delete input { width: 24px; height: 24px; background-image: url([[pix:t/delete]]); margin: .3em .3em 0 .3em; } .gradingform_rubric.editor .criterion .controls .duplicate input { width: 24px; height: 24px; background-image: url([[pix:t/copy]]); margin: .3em .3em 0 .3em; } .gradingform_rubric.editor .levels .level .delete input { width: 24px; height: 24px; background-image: url([[pix:t/delete]]); } .gradingform_rubric.editor .moveup input { width: 24px; height: 24px; background-image: url([[pix:t/up]]); margin: .3em .3em 0 .3em; } .gradingform_rubric.editor .movedown input { width: 24px; height: 24px; background-image: url([[pix:t/down]]); margin: .3em .3em 0 .3em; } .gradingform_rubric.editor .addcriterion input, .gradingform_rubric.editor .addlevel input { background: #fff url([[pix:t/add]]) 4px 6px / 16px 16px no-repeat; display: block; color: #555; font-weight: bold; text-decoration: none; } .gradingform_rubric.editor .addcriterion input { height: 30px; line-height: 29px; margin-bottom: 14px; padding-left: 20px; padding-right: 10px; } .gradingform_rubric.editor .addlevel input { padding-left: 24px; padding-right: 8px; } .gradingform_rubric .options .optionsheading { font-weight: bold; font-size: 1.1em; padding-bottom: 5px; } .gradingform_rubric .options .option { padding-bottom: 2px; } .gradingform_rubric .options .option label { margin-left: 5px; } .gradingform_rubric .options .option .value { margin-left: 5px; font-weight: bold; } .gradingform_rubric .criterion .levels.error { border: 1px solid red; } .gradingform_rubric .criterion .description.error, .gradingform_rubric .criterion .levels .level .definition.error, .gradingform_rubric .criterion .levels .level .score.error { background: #fdd; } .gradingform_rubric-regrade { padding: 10px; background: #fdd; border: 1px solid #f00; margin-bottom: 10px; } .gradingform_rubric-restored { padding: 10px; background: #ffd; border: 1px solid #ff0; margin-bottom: 10px; } .gradingform_rubric-error { color: red; font-weight: bold; } /* special classes for elements created by rubriceditor.js */ .gradingform_rubric.editor .hiddenelement { display: none; } .gradingform_rubric.editor .pseudotablink { background-color: transparent; border: 0 solid; height: 1px; width: 1px; color: transparent; padding: 0; margin: 0; position: relative; float: right; } .gradingpanel-gradingform_rubric [aria-checked="true"] { border: 1px solid black; } PK'\QC݊݊form/rubric/renderer.phpnu[. /** * Contains renderer used for displaying rubric * * @package gradingform_rubric * @copyright 2011 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Grading method plugin renderer * * @package gradingform_rubric * @copyright 2011 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradingform_rubric_renderer extends plugin_renderer_base { /** * This function returns html code for displaying criterion. Depending on $mode it may be the * code to edit rubric, to preview the rubric, to evaluate somebody or to review the evaluation. * * This function may be called from display_rubric() to display the whole rubric, or it can be * called by itself to return a template used by JavaScript to add new empty criteria to the * rubric being designed. * In this case it will use macros like {NAME}, {LEVELS}, {CRITERION-id}, etc. * * When overriding this function it is very important to remember that all elements of html * form (in edit or evaluate mode) must have the name $elementname. * * Also JavaScript relies on the class names of elements and when developer changes them * script might stop working. * * @param int $mode rubric display mode, see {@link gradingform_rubric_controller} * @param array $options display options for this rubric, defaults are: {@link gradingform_rubric_controller::get_default_options()} * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) * @param array|null $criterion criterion data * @param string $levelsstr evaluated templates for this criterion levels * @param array|null $value (only in view mode) teacher's feedback on this criterion * @return string */ public function criterion_template($mode, $options, $elementname = '{NAME}', $criterion = null, $levelsstr = '{LEVELS}', $value = null) { // TODO MDL-31235 description format, remark format if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) { $criterion = array('id' => '{CRITERION-id}', 'description' => '{CRITERION-description}', 'sortorder' => '{CRITERION-sortorder}', 'class' => '{CRITERION-class}'); } else { foreach (array('sortorder', 'description', 'class') as $key) { // set missing array elements to empty strings to avoid warnings if (!array_key_exists($key, $criterion)) { $criterion[$key] = ''; } } } $criteriontemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $criterion['class'], 'id' => '{NAME}-criteria-{CRITERION-id}')); if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) { $criteriontemplate .= html_writer::start_tag('td', array('class' => 'controls')); foreach (array('moveup', 'delete', 'movedown', 'duplicate') as $key) { $value = get_string('criterion'.$key, 'gradingform_rubric'); $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}]['.$key.']', 'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value)); $criteriontemplate .= html_writer::tag('div', $button, array('class' => $key)); } $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder'])); $criteriontemplate .= html_writer::end_tag('td'); // .controls // Criterion description text area. $descriptiontextareaparams = array( 'name' => '{NAME}[criteria][{CRITERION-id}][description]', 'id' => '{NAME}-criteria-{CRITERION-id}-description', 'aria-label' => get_string('criterion', 'gradingform_rubric', ''), 'cols' => '10', 'rows' => '5' ); $description = html_writer::tag('textarea', s($criterion['description']), $descriptiontextareaparams); } else { if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) { $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder'])); $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][description]', 'value' => $criterion['description'])); } $description = s($criterion['description']); } $descriptionclass = 'description'; if (isset($criterion['error_description'])) { $descriptionclass .= ' error'; } // Description cell params. $descriptiontdparams = array( 'class' => $descriptionclass, 'id' => '{NAME}-criteria-{CRITERION-id}-description-cell' ); if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL && $mode != gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) { // Set description's cell as tab-focusable. $descriptiontdparams['tabindex'] = '0'; // Set label for the criterion cell. $descriptiontdparams['aria-label'] = get_string('criterion', 'gradingform_rubric', s($criterion['description'])); } // Default value for criterion ids. // Edge case: submitting empty grade when remark field is disabled. // Reason: we need the criteria keys for the clear_attempt to clear the rubric fillings. if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) { $criteriontemplate .= html_writer::empty_tag('input', [ 'type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][]', ]); } // Description cell. $criteriontemplate .= html_writer::tag('td', $description, $descriptiontdparams); // Levels table. $levelsrowparams = [ 'id' => '{NAME}-criteria-{CRITERION-id}-levels', 'aria-label' => get_string('levelsgroup', 'gradingform_rubric'), ]; // Add radiogroup role only when not previewing or editing. $isradiogroup = !in_array($mode, [ gradingform_rubric_controller::DISPLAY_EDIT_FULL, gradingform_rubric_controller::DISPLAY_EDIT_FROZEN, gradingform_rubric_controller::DISPLAY_PREVIEW, gradingform_rubric_controller::DISPLAY_PREVIEW_GRADED, ]); $levelsrowparams['role'] = $isradiogroup ? 'radiogroup' : 'list'; $levelsrow = html_writer::tag('tr', $levelsstr, $levelsrowparams); $levelstableparams = [ 'id' => '{NAME}-criteria-{CRITERION-id}-levels-table', 'role' => 'none', ]; $levelsstrtable = html_writer::tag('table', $levelsrow, $levelstableparams); $levelsclass = 'levels'; if (isset($criterion['error_levels'])) { $levelsclass .= ' error'; } $criteriontemplate .= html_writer::tag('td', $levelsstrtable, array('class' => $levelsclass)); if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) { $value = get_string('criterionaddlevel', 'gradingform_rubric'); $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][addlevel]', 'id' => '{NAME}-criteria-{CRITERION-id}-levels-addlevel', 'value' => $value, 'class' => 'btn btn-secondary')); $criteriontemplate .= html_writer::tag('td', $button, array('class' => 'addlevel')); } $displayremark = ($options['enableremarks'] && ($mode != gradingform_rubric_controller::DISPLAY_VIEW || $options['showremarksstudent'])); if ($displayremark) { $currentremark = ''; if (isset($value['remark'])) { $currentremark = $value['remark']; } // Label for criterion remark. $remarkinfo = new stdClass(); $remarkinfo->description = s($criterion['description']); $remarkinfo->remark = $currentremark; $remarklabeltext = get_string('criterionremark', 'gradingform_rubric', $remarkinfo); if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) { // HTML parameters for remarks text area. $remarkparams = array( 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'id' => '{NAME}-criteria-{CRITERION-id}-remark', 'cols' => '10', 'rows' => '5', 'aria-label' => $remarklabeltext ); $input = html_writer::tag('textarea', s($currentremark), $remarkparams); $criteriontemplate .= html_writer::tag('td', $input, array('class' => 'remark')); } else if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN) { $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark)); }else if ($mode == gradingform_rubric_controller::DISPLAY_REVIEW || $mode == gradingform_rubric_controller::DISPLAY_VIEW) { // HTML parameters for remarks cell. $remarkparams = array( 'class' => 'remark', 'tabindex' => '0', 'id' => '{NAME}-criteria-{CRITERION-id}-remark', 'aria-label' => $remarklabeltext ); $criteriontemplate .= html_writer::tag('td', s($currentremark), $remarkparams); } } $criteriontemplate .= html_writer::end_tag('tr'); // .criterion $criteriontemplate = str_replace('{NAME}', $elementname, $criteriontemplate); $criteriontemplate = str_replace('{CRITERION-id}', $criterion['id'], $criteriontemplate); return $criteriontemplate; } /** * This function returns html code for displaying one level of one criterion. Depending on $mode * it may be the code to edit rubric, to preview the rubric, to evaluate somebody or to review the evaluation. * * This function may be called from display_rubric() to display the whole rubric, or it can be * called by itself to return a template used by JavaScript to add new empty level to the * criterion during the design of rubric. * In this case it will use macros like {NAME}, {CRITERION-id}, {LEVEL-id}, etc. * * When overriding this function it is very important to remember that all elements of html * form (in edit or evaluate mode) must have the name $elementname. * * Also JavaScript relies on the class names of elements and when developer changes them * script might stop working. * * @param int $mode rubric display mode see {@link gradingform_rubric_controller} * @param array $options display options for this rubric, defaults are: {@link gradingform_rubric_controller::get_default_options()} * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) * @param string|int $criterionid either id of the nesting criterion or a macro for template * @param array|null $level level data, also in view mode it might also have property $level['checked'] whether this level is checked * @return string */ public function level_template($mode, $options, $elementname = '{NAME}', $criterionid = '{CRITERION-id}', $level = null) { // TODO MDL-31235 definition format if (!isset($level['id'])) { $level = array('id' => '{LEVEL-id}', 'definition' => '{LEVEL-definition}', 'score' => '{LEVEL-score}', 'class' => '{LEVEL-class}', 'checked' => false); } else { foreach (array('score', 'definition', 'class', 'checked', 'index') as $key) { // set missing array elements to empty strings to avoid warnings if (!array_key_exists($key, $level)) { $level[$key] = ''; } } } // Get level index. $levelindex = isset($level['index']) ? $level['index'] : '{LEVEL-index}'; // Template for one level within one criterion $tdattributes = array( 'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}', 'class' => 'text-break level' . $level['class'] ); if (isset($level['tdwidth'])) { $tdattributes['style'] = "width: " . round($level['tdwidth']).'%;'; } $leveltemplate = html_writer::start_tag('div', array('class' => 'level-wrapper')); if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) { $definitionparams = array( 'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'aria-label' => get_string('leveldefinition', 'gradingform_rubric', $levelindex), 'cols' => '10', 'rows' => '4' ); $definition = html_writer::tag('textarea', s($level['definition']), $definitionparams); $scoreparams = array( 'type' => 'text', 'id' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'aria-label' => get_string('scoreinputforlevel', 'gradingform_rubric', $levelindex), 'size' => '3', 'value' => $level['score'] ); $score = html_writer::empty_tag('input', $scoreparams); } else { if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) { $leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'value' => $level['definition'])); $leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'value' => $level['score'])); } $definition = s($level['definition']); $score = $level['score']; } if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) { $levelradioparams = array( 'type' => 'radio', 'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition', 'name' => '{NAME}[criteria][{CRITERION-id}][levelid]', 'value' => $level['id'] ); if ($level['checked']) { $levelradioparams['checked'] = 'checked'; } $input = html_writer::empty_tag('input', $levelradioparams); $leveltemplate .= html_writer::div($input, 'radio'); } if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN && $level['checked']) { $leveltemplate .= html_writer::empty_tag('input', array( 'type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levelid]', 'value' => $level['id'] ) ); } $score = html_writer::tag('span', $score, array('id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-score', 'class' => 'scorevalue')); $definitionclass = 'definition'; if (isset($level['error_definition'])) { $definitionclass .= ' error'; } if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL && $mode != gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) { $tdattributes['tabindex'] = '0'; $levelinfo = new stdClass(); $levelinfo->definition = s($level['definition']); $levelinfo->score = $level['score']; $tdattributes['aria-label'] = get_string('level', 'gradingform_rubric', $levelinfo); if ($mode != gradingform_rubric_controller::DISPLAY_PREVIEW && $mode != gradingform_rubric_controller::DISPLAY_PREVIEW_GRADED) { // Add role of radio button to level cell if not in edit and preview mode. $tdattributes['role'] = 'radio'; if ($level['checked']) { $tdattributes['aria-checked'] = 'true'; } else { $tdattributes['aria-checked'] = 'false'; } } else { $tdattributes['role'] = 'listitem'; } } else { $tdattributes['role'] = 'listitem'; } $leveltemplateparams = array( 'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition-container' ); $leveltemplate .= html_writer::div($definition, $definitionclass, $leveltemplateparams); $displayscore = true; if (!$options['showscoreteacher'] && in_array($mode, array(gradingform_rubric_controller::DISPLAY_EVAL, gradingform_rubric_controller::DISPLAY_EVAL_FROZEN, gradingform_rubric_controller::DISPLAY_REVIEW))) { $displayscore = false; } if (!$options['showscorestudent'] && in_array($mode, array(gradingform_rubric_controller::DISPLAY_VIEW, gradingform_rubric_controller::DISPLAY_PREVIEW_GRADED))) { $displayscore = false; } if ($displayscore) { $scoreclass = 'score d-inline'; if (isset($level['error_score'])) { $scoreclass .= ' error'; } $leveltemplate .= html_writer::tag('div', get_string('scorepostfix', 'gradingform_rubric', $score), array('class' => $scoreclass)); } if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) { $value = get_string('leveldelete', 'gradingform_rubric', $levelindex); $buttonparams = array( 'type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][delete]', 'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-delete', 'value' => $value ); $button = html_writer::empty_tag('input', $buttonparams); $leveltemplate .= html_writer::tag('div', $button, array('class' => 'delete')); } $leveltemplate .= html_writer::end_tag('div'); // .level-wrapper $leveltemplate = html_writer::tag('td', $leveltemplate, $tdattributes); // The .level cell. $leveltemplate = str_replace('{NAME}', $elementname, $leveltemplate); $leveltemplate = str_replace('{CRITERION-id}', $criterionid, $leveltemplate); $leveltemplate = str_replace('{LEVEL-id}', $level['id'], $leveltemplate); return $leveltemplate; } /** * This function returns html code for displaying rubric template (content before and after * criteria list). Depending on $mode it may be the code to edit rubric, to preview the rubric, * to evaluate somebody or to review the evaluation. * * This function is called from display_rubric() to display the whole rubric. * * When overriding this function it is very important to remember that all elements of html * form (in edit or evaluate mode) must have the name $elementname. * * Also JavaScript relies on the class names of elements and when developer changes them * script might stop working. * * @param int $mode rubric display mode see {@link gradingform_rubric_controller} * @param array $options display options for this rubric, defaults are: {@link gradingform_rubric_controller::get_default_options()} * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) * @param string $criteriastr evaluated templates for this rubric's criteria * @return string */ protected function rubric_template($mode, $options, $elementname, $criteriastr) { $classsuffix = ''; // CSS suffix for class of the main div. Depends on the mode switch ($mode) { case gradingform_rubric_controller::DISPLAY_EDIT_FULL: $classsuffix = ' editor editable'; break; case gradingform_rubric_controller::DISPLAY_EDIT_FROZEN: $classsuffix = ' editor frozen'; break; case gradingform_rubric_controller::DISPLAY_PREVIEW: case gradingform_rubric_controller::DISPLAY_PREVIEW_GRADED: $classsuffix = ' editor preview'; break; case gradingform_rubric_controller::DISPLAY_EVAL: $classsuffix = ' evaluate editable'; break; case gradingform_rubric_controller::DISPLAY_EVAL_FROZEN: $classsuffix = ' evaluate frozen'; break; case gradingform_rubric_controller::DISPLAY_REVIEW: $classsuffix = ' review'; break; case gradingform_rubric_controller::DISPLAY_VIEW: $classsuffix = ' view'; break; } $rubrictemplate = html_writer::start_tag('div', array('id' => 'rubric-{NAME}', 'class' => 'clearfix gradingform_rubric'.$classsuffix)); // Rubric table. $rubrictableparams = [ 'class' => 'criteria', 'id' => '{NAME}-criteria', ]; $caption = html_writer::tag('caption', get_string('rubric', 'gradingform_rubric'), ['class' => 'visually-hidden']); $rubrictable = html_writer::tag('table', $caption . $criteriastr, $rubrictableparams); $rubrictemplate .= $rubrictable; if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) { $value = get_string('addcriterion', 'gradingform_rubric'); $criteriainputparams = array( 'type' => 'submit', 'name' => '{NAME}[criteria][addcriterion]', 'id' => '{NAME}-criteria-addcriterion', 'value' => $value ); $input = html_writer::empty_tag('input', $criteriainputparams); $rubrictemplate .= html_writer::tag('div', $input, array('class' => 'addcriterion btn btn-secondary')); } $rubrictemplate .= $this->rubric_edit_options($mode, $options); $rubrictemplate .= html_writer::end_tag('div'); return str_replace('{NAME}', $elementname, $rubrictemplate); } /** * Generates html template to view/edit the rubric options. Expression {NAME} is used in * template for the form element name * * @param int $mode rubric display mode see {@link gradingform_rubric_controller} * @param array $options display options for this rubric, defaults are: {@link gradingform_rubric_controller::get_default_options()} * @return string */ protected function rubric_edit_options($mode, $options) { if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL && $mode != gradingform_rubric_controller::DISPLAY_EDIT_FROZEN && $mode != gradingform_rubric_controller::DISPLAY_PREVIEW) { // Options are displayed only for people who can manage return; } $html = html_writer::start_tag('div', array('class' => 'options')); $html .= html_writer::tag('div', get_string('rubricoptions', 'gradingform_rubric'), array('class' => 'optionsheading')); $attrs = array('type' => 'hidden', 'name' => '{NAME}[options][optionsset]', 'value' => 1); foreach ($options as $option => $value) { $html .= html_writer::start_tag('div', array('class' => 'option '.$option)); $attrs = array('name' => '{NAME}[options]['.$option.']', 'id' => '{NAME}-options-'.$option); switch ($option) { case 'sortlevelsasc': // Display option as dropdown $html .= html_writer::label(get_string($option, 'gradingform_rubric'), $attrs['id'], false); $value = (int)(!!$value); // make sure $value is either 0 or 1 if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) { $selectoptions = array(0 => get_string($option.'0', 'gradingform_rubric'), 1 => get_string($option.'1', 'gradingform_rubric')); $valuestr = html_writer::select($selectoptions, $attrs['name'], $value, false, array('id' => $attrs['id'])); $html .= html_writer::tag('span', $valuestr, array('class' => 'value')); } else { $html .= html_writer::tag('span', get_string($option.$value, 'gradingform_rubric'), array('class' => 'value')); if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) { $html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value)); } } break; default: if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN && $value) { // Id should be different then the actual input added later. $attrs['id'] .= '_hidden'; $html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value)); } // Display option as checkbox $attrs['type'] = 'checkbox'; $attrs['value'] = 1; if ($value) { $attrs['checked'] = 'checked'; } if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN || $mode == gradingform_rubric_controller::DISPLAY_PREVIEW) { $attrs['disabled'] = 'disabled'; unset($attrs['name']); // Id should be different then the actual input added later. $attrs['id'] .= '_disabled'; } $html .= html_writer::empty_tag('input', $attrs); $html .= html_writer::tag('label', get_string($option, 'gradingform_rubric'), array('for' => $attrs['id'])); break; } if (get_string_manager()->string_exists($option.'_help', 'gradingform_rubric')) { $html .= $this->help_icon($option, 'gradingform_rubric'); } $html .= html_writer::end_tag('div'); // .option } $html .= html_writer::end_tag('div'); // .options return $html; } /** * This function returns html code for displaying rubric. Depending on $mode it may be the code * to edit rubric, to preview the rubric, to evaluate somebody or to review the evaluation. * * It is very unlikely that this function needs to be overriden by theme. It does not produce * any html code, it just prepares data about rubric design and evaluation, adds the CSS * class to elements and calls the functions level_template, criterion_template and * rubric_template * * @param array $criteria data about the rubric design * @param array $options display options for this rubric, defaults are: {@link gradingform_rubric_controller::get_default_options()} * @param int $mode rubric display mode, see {@link gradingform_rubric_controller} * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) * @param array $values evaluation result * @return string */ public function display_rubric($criteria, $options, $mode, $elementname = null, $values = null) { $criteriastr = ''; $cnt = 0; foreach ($criteria as $id => $criterion) { $criterion['class'] = $this->get_css_class_suffix($cnt++, sizeof($criteria) -1); $criterion['id'] = $id; $levelsstr = ''; $levelcnt = 0; if (isset($values['criteria'][$id])) { $criterionvalue = $values['criteria'][$id]; } else { $criterionvalue = null; } $index = 1; foreach ($criterion['levels'] as $levelid => $level) { $level['id'] = $levelid; $level['class'] = $this->get_css_class_suffix($levelcnt++, sizeof($criterion['levels']) -1); $level['checked'] = (isset($criterionvalue['levelid']) && ((int)$criterionvalue['levelid'] === $levelid)); if ($level['checked'] && ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN || $mode == gradingform_rubric_controller::DISPLAY_REVIEW || $mode == gradingform_rubric_controller::DISPLAY_VIEW)) { $level['class'] .= ' checked'; //in mode DISPLAY_EVAL the class 'checked' will be added by JS if it is enabled. If JS is not enabled, the 'checked' class will only confuse } if (isset($criterionvalue['savedlevelid']) && ((int)$criterionvalue['savedlevelid'] === $levelid)) { $level['class'] .= ' currentchecked'; } $level['tdwidth'] = 100/count($criterion['levels']); $level['index'] = $index; $levelsstr .= $this->level_template($mode, $options, $elementname, $id, $level); $index++; } $criteriastr .= $this->criterion_template($mode, $options, $elementname, $criterion, $levelsstr, $criterionvalue); } return $this->rubric_template($mode, $options, $elementname, $criteriastr); } /** * Help function to return CSS class names for element (first/last/even/odd) with leading space * * @param int $idx index of this element in the row/column * @param int $maxidx maximum index of the element in the row/column * @return string */ protected function get_css_class_suffix($idx, $maxidx) { $class = ''; if ($idx == 0) { $class .= ' first'; } if ($idx == $maxidx) { $class .= ' last'; } if ($idx%2) { $class .= ' odd'; } else { $class .= ' even'; } return $class; } /** * Displays for the student the list of instances or default content if no instances found * * @param array $instances array of objects of type gradingform_rubric_instance * @param string $defaultcontent default string that would be displayed without advanced grading * @param boolean $cangrade whether current user has capability to grade in this context * @return string */ public function display_instances($instances, $defaultcontent, $cangrade) { $return = ''; if (sizeof($instances)) { $return .= html_writer::start_tag('div', array('class' => 'advancedgrade')); $idx = 0; foreach ($instances as $instance) { $return .= $this->display_instance($instance, $idx++, $cangrade); } $return .= html_writer::end_tag('div'); } return $return. $defaultcontent; } /** * Displays one grading instance * * @param gradingform_rubric_instance $instance * @param int $idx unique number of instance on page * @param bool $cangrade whether current user has capability to grade in this context */ public function display_instance(gradingform_rubric_instance $instance, $idx, $cangrade) { $criteria = $instance->get_controller()->get_definition()->rubric_criteria; $options = $instance->get_controller()->get_options(); $values = $instance->get_rubric_filling(); if ($cangrade) { $mode = gradingform_rubric_controller::DISPLAY_REVIEW; $showdescription = $options['showdescriptionteacher']; } else { $mode = gradingform_rubric_controller::DISPLAY_VIEW; $showdescription = $options['showdescriptionstudent']; } $output = ''; if ($showdescription) { $output .= $this->box($instance->get_controller()->get_formatted_description(), 'gradingform_rubric-description'); } $output .= $this->display_rubric($criteria, $options, $mode, 'rubric'.$idx, $values); return $output; } /** * Displays confirmation that students require re-grading * * @param string $elementname * @param int $changelevel * @param string $value * @return string */ public function display_regrade_confirmation($elementname, $changelevel, $value) { $html = html_writer::start_tag('div', array('class' => 'gradingform_rubric-regrade', 'role' => 'alert')); if ($changelevel<=2) { $html .= html_writer::label(get_string('regrademessage1', 'gradingform_rubric'), 'menu' . $elementname . 'regrade'); $selectoptions = array( 0 => get_string('regradeoption0', 'gradingform_rubric'), 1 => get_string('regradeoption1', 'gradingform_rubric') ); $html .= html_writer::select($selectoptions, $elementname.'[regrade]', $value, false); } else { $html .= get_string('regrademessage5', 'gradingform_rubric'); $html .= html_writer::empty_tag('input', array('name' => $elementname.'[regrade]', 'value' => 1, 'type' => 'hidden')); } $html .= html_writer::end_tag('div'); return $html; } /** * Generates and returns HTML code to display information box about how rubric score is converted to the grade * * @param array $scores * @return string */ public function display_rubric_mapping_explained($scores) { $html = ''; if (!$scores) { return $html; } if ($scores['minscore'] <> 0) { $html .= $this->output->notification(get_string('zerolevelsabsent', 'gradingform_rubric'), 'error'); } $html .= $this->output->notification(get_string('rubricmappingexplained', 'gradingform_rubric', (object)$scores), 'info'); return $html; } } PK'\Z|OOform/rubric/db/services.phpnu[. /** * Rubric external functions and service definitions. * * @package gradingform_rubric * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $functions = [ 'gradingform_rubric_grader_gradingpanel_fetch' => [ 'classname' => 'gradingform_rubric\\grades\\grader\\gradingpanel\\external\\fetch', 'description' => 'Fetch the data required to display the grader grading panel, ' . 'creating the grade item if required', 'type' => 'write', 'ajax' => true, ], 'gradingform_rubric_grader_gradingpanel_store' => [ 'classname' => 'gradingform_rubric\\grades\\grader\\gradingpanel\\external\\store', 'description' => 'Store the grading data for a user from the grader grading panel.', 'type' => 'write', 'ajax' => true, ], ]; PK'\form/rubric/db/upgrade.phpnu[. /** * This file keeps track of upgrades to plugin gradingform_rubric * * @package gradingform_rubric * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Keeps track or rubric plugin upgrade path * * @param int $oldversion the DB version of currently installed plugin * @return bool true */ function xmldb_gradingform_rubric_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; } PK'\eform/rubric/db/install.xmlnu[
PK'\*form/rubric/lang/en/gradingform_rubric.phpnu[. /** * Language file for plugin gradingform_rubric * * @package gradingform_rubric * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['addcriterion'] = 'Add criterion'; $string['additionalfeedback'] = 'Additional feedback'; $string['alwaysshowdefinition'] = 'Allow users to preview rubric (otherwise it will only be displayed after grading)'; $string['backtoediting'] = 'Back to editing'; $string['confirmdeletecriterion'] = 'Are you sure you want to delete this criterion?'; $string['confirmdeletelevel'] = 'Are you sure you want to delete this level?'; $string['criterion'] = 'Criterion {$a}'; $string['criterionaddlevel'] = 'Add level'; $string['criteriondelete'] = 'Delete criterion'; $string['criterionduplicate'] = 'Duplicate criterion'; $string['criterionempty'] = 'Click to edit criterion'; $string['criterionmovedown'] = 'Move down'; $string['criterionmoveup'] = 'Move up'; $string['criterionremark'] = 'Remark for criterion {$a->description}: {$a->remark}'; $string['definerubric'] = 'Define rubric'; $string['description'] = 'Description'; $string['enableremarks'] = 'Allow grader to add text remarks for each criterion'; $string['err_mintwolevels'] = 'Each criterion must have at least two levels'; $string['err_nocriteria'] = 'Rubric must contain at least one criterion'; $string['err_nodefinition'] = 'Level definition can not be empty'; $string['err_nodescription'] = 'Criterion description can not be empty'; $string['err_novariations'] = 'Criterion levels cannot all be worth the same number of points'; $string['err_scoreformat'] = 'Number of points for each level must be a valid number'; $string['err_totalscore'] = 'Maximum number of points possible when graded by the rubric must be more than zero'; $string['gradingof'] = '{$a} grading'; $string['level'] = 'Level {$a->definition}, {$a->score} points.'; $string['leveldelete'] = 'Delete level {$a}'; $string['leveldefinition'] = 'Level {$a} definition'; $string['levelempty'] = 'Click to edit level'; $string['levelsgroup'] = 'Levels group'; $string['lockzeropoints'] = 'Calculate grade having a minimum score of the minimum achievable grade for the rubric'; $string['lockzeropoints_help'] = 'This setting only applies if the sum of the minimum number of points for each criterion is greater than 0. If ticked, the minimum score of the activity will be the minimum achievable grade for the rubric. If unticked, the minimum possible score for the rubric will be mapped to the minimum grade available for the activity (which is 0 unless a scale is used).'; $string['name'] = 'Name'; $string['needregrademessage'] = 'The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade.'; $string['notset'] = 'Not set'; $string['pluginname'] = 'Rubric'; $string['pointsvalue'] = '{$a} points'; $string['previewrubric'] = 'Preview rubric'; $string['privacy:metadata:criterionid'] = 'An identifier for a specific criterion being graded.'; $string['privacy:metadata:fillingssummary'] = 'Stores information about the user\'s grade created by the rubric.'; $string['privacy:metadata:instanceid'] = 'An identifier relating to a grade in an activity.'; $string['privacy:metadata:levelid'] = 'The level obtained in the rubric.'; $string['privacy:metadata:remark'] = 'Remarks related to the rubric criterion being assessed.'; $string['regrademessage1'] = 'You are about to save changes to a rubric that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the rubric will be hidden from students until their item is regraded.'; $string['regrademessage5'] = 'You are about to save significant changes to a rubric that has already been used for grading. The gradebook value will be unchanged, but the rubric will be hidden from students until their item is regraded.'; $string['regradeoption0'] = 'Do not mark for regrade'; $string['regradeoption1'] = 'Mark for regrade'; $string['restoredfromdraft'] = 'NOTE: The last attempt to grade this person was not saved properly so draft grades have been restored. If you want to cancel these changes use the \'Cancel\' button below.'; $string['rubric'] = 'Rubric'; $string['rubricmapping'] = 'Score to grade mapping rules'; $string['rubricmappingexplained'] = 'The minimum possible score for this rubric is {$a->minscore} points. It will be converted to the minimum grade available for the activity (which is 0 unless a scale is used). The maximum score of {$a->maxscore} points will be converted to the maximum grade. Intermediate scores will be converted respectively. If a scale is used for grading, the score will be rounded and converted to the scale elements as if they were consecutive integers. This grade calculation may be changed by editing the form and ticking the box \'Calculate grade having a minimum score of the minimum achievable grade for the rubric\'.'; $string['rubricnotcompleted'] = 'Please choose something for each criterion'; $string['rubricoptions'] = 'Rubric options'; $string['rubricstatus'] = 'Current rubric status'; $string['save'] = 'Save'; $string['saverubric'] = 'Save rubric and make it ready'; $string['saverubricdraft'] = 'Save as draft'; $string['scoreinputforlevel'] = 'Score input for level {$a}'; $string['scorepostfix'] = '{$a} points'; $string['showdescriptionstudent'] = 'Display rubric description to those being graded'; $string['showdescriptionteacher'] = 'Display rubric description during evaluation'; $string['showremarksstudent'] = 'Show remarks to those being graded'; $string['showscorestudent'] = 'Display points for each level to those being graded'; $string['showscoreteacher'] = 'Display points for each level during evaluation'; $string['sortlevelsasc'] = 'Sort order for levels:'; $string['sortlevelsasc0'] = 'Descending by number of points'; $string['sortlevelsasc1'] = 'Ascending by number of points'; $string['zerolevelsabsent'] = 'Warning: The minimum possible score for this rubric is not 0; this can result in unexpected grades for the activity. To avoid this, each criterion should have a level with 0 points.
This warning may be ignored if a scale is used for grading, and the minimum levels in the rubric correspond to the minimum value of the scale.'; PK'\(qqEform/rubric/backup/moodle2/backup_gradingform_rubric_plugin.class.phpnu[. /** * Support for backup API * * @package gradingform_rubric * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Defines rubric backup structures * * @package gradingform_rubric * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_gradingform_rubric_plugin extends backup_gradingform_plugin { /** * Declares rubric structures to append to the grading form definition */ protected function define_definition_plugin_structure() { // Append data only if the grand-parent element has 'method' set to 'rubric' $plugin = $this->get_plugin_element(null, '../../method', 'rubric'); // Create a visible container for our data $pluginwrapper = new backup_nested_element($this->get_recommended_name()); // Connect our visible container to the parent $plugin->add_child($pluginwrapper); // Define our elements $criteria = new backup_nested_element('criteria'); $criterion = new backup_nested_element('criterion', array('id'), array( 'sortorder', 'description', 'descriptionformat')); $levels = new backup_nested_element('levels'); $level = new backup_nested_element('level', array('id'), array( 'score', 'definition', 'definitionformat')); // Build elements hierarchy $pluginwrapper->add_child($criteria); $criteria->add_child($criterion); $criterion->add_child($levels); $levels->add_child($level); // Set sources to populate the data $criterion->set_source_table('gradingform_rubric_criteria', array('definitionid' => backup::VAR_PARENTID)); $level->set_source_table('gradingform_rubric_levels', array('criterionid' => backup::VAR_PARENTID)); // no need to annotate ids or files yet (one day when criterion definition supports // embedded files, they must be annotated here) return $plugin; } /** * Declares rubric structures to append to the grading form instances */ protected function define_instance_plugin_structure() { // Append data only if the ancestor 'definition' element has 'method' set to 'rubric' $plugin = $this->get_plugin_element(null, '../../../../method', 'rubric'); // Create a visible container for our data $pluginwrapper = new backup_nested_element($this->get_recommended_name()); // Connect our visible container to the parent $plugin->add_child($pluginwrapper); // Define our elements $fillings = new backup_nested_element('fillings'); $filling = new backup_nested_element('filling', array('id'), array( 'criterionid', 'levelid', 'remark', 'remarkformat')); // Build elements hierarchy $pluginwrapper->add_child($fillings); $fillings->add_child($filling); // Set sources to populate the data // Binding criterionid to ensure it's existence $filling->set_source_sql('SELECT rf.* FROM {gradingform_rubric_fillings} rf JOIN {grading_instances} gi ON gi.id = rf.instanceid JOIN {gradingform_rubric_criteria} rc ON rc.id = rf.criterionid AND gi.definitionid = rc.definitionid WHERE rf.instanceid = :instanceid', array('instanceid' => backup::VAR_PARENTID)); // no need to annotate ids or files yet (one day when remark field supports // embedded fileds, they must be annotated here) return $plugin; } } PK'\6Fform/rubric/backup/moodle2/restore_gradingform_rubric_plugin.class.phpnu[. /** * Support for restore API * * @package gradingform_rubric * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Restores the rubric specific data from grading.xml file * * @package gradingform_rubric * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_gradingform_rubric_plugin extends restore_gradingform_plugin { /** * Declares the rubric XML paths attached to the form definition element * * @return array of {@link restore_path_element} */ protected function define_definition_plugin_structure() { $paths = array(); $paths[] = new restore_path_element('gradingform_rubric_criterion', $this->get_pathfor('/criteria/criterion')); $paths[] = new restore_path_element('gradingform_rubric_level', $this->get_pathfor('/criteria/criterion/levels/level')); return $paths; } /** * Declares the rubric XML paths attached to the form instance element * * @return array of {@link restore_path_element} */ protected function define_instance_plugin_structure() { $paths = array(); $paths[] = new restore_path_element('gradinform_rubric_filling', $this->get_pathfor('/fillings/filling')); return $paths; } /** * Processes criterion element data * * Sets the mapping 'gradingform_rubric_criterion' to be used later by * {@link self::process_gradinform_rubric_filling()} * * @param stdClass|array $data */ public function process_gradingform_rubric_criterion($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->definitionid = $this->get_new_parentid('grading_definition'); $newid = $DB->insert_record('gradingform_rubric_criteria', $data); $this->set_mapping('gradingform_rubric_criterion', $oldid, $newid); } /** * Processes level element data * * Sets the mapping 'gradingform_rubric_level' to be used later by * {@link self::process_gradinform_rubric_filling()} * * @param stdClass|array $data */ public function process_gradingform_rubric_level($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->criterionid = $this->get_new_parentid('gradingform_rubric_criterion'); $newid = $DB->insert_record('gradingform_rubric_levels', $data); $this->set_mapping('gradingform_rubric_level', $oldid, $newid); } /** * Processes filling element data * * @param stdClass|array $data */ public function process_gradinform_rubric_filling($data) { global $DB; $data = (object)$data; $data->instanceid = $this->get_new_parentid('grading_instance'); $data->criterionid = $this->get_mappingid('gradingform_rubric_criterion', $data->criterionid); $data->levelid = $this->get_mappingid('gradingform_rubric_level', $data->levelid); if (!empty($data->criterionid)) { $DB->insert_record('gradingform_rubric_fillings', $data); } } } PK'\uy+##form/rubric/edit_form.phpnu[. /** * The form used at the rubric editor page is defined here * * @package gradingform_rubric * @copyright 2011 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/lib/formslib.php'); require_once(__DIR__.'/rubriceditor.php'); MoodleQuickForm::registerElementType('rubriceditor', $CFG->dirroot.'/grade/grading/form/rubric/rubriceditor.php', 'MoodleQuickForm_rubriceditor'); /** * Defines the rubric edit form * * @package gradingform_rubric * @copyright 2011 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradingform_rubric_editrubric extends moodleform { /** * Form element definition */ public function definition() { $form = $this->_form; $form->addElement('hidden', 'areaid'); $form->setType('areaid', PARAM_INT); $form->addElement('hidden', 'returnurl'); $form->setType('returnurl', PARAM_LOCALURL); // name $form->addElement('text', 'name', get_string('name', 'gradingform_rubric'), array('size' => 52, 'aria-required' => 'true')); $form->addRule('name', get_string('required'), 'required', null, 'client'); $form->setType('name', PARAM_TEXT); // description $options = gradingform_rubric_controller::description_form_field_options($this->_customdata['context']); $form->addElement('editor', 'description_editor', get_string('description', 'gradingform_rubric'), null, $options); $form->setType('description_editor', PARAM_RAW); // rubric completion status $choices = array(); $choices[gradingform_controller::DEFINITION_STATUS_DRAFT] = html_writer::tag('span', get_string('statusdraft', 'core_grading'), array('class' => 'status draft')); $choices[gradingform_controller::DEFINITION_STATUS_READY] = html_writer::tag('span', get_string('statusready', 'core_grading'), array('class' => 'status ready')); $form->addElement('select', 'status', get_string('rubricstatus', 'gradingform_rubric'), $choices)->freeze(); // rubric editor $form->addElement('rubriceditor', 'rubric', get_string('rubric', 'gradingform_rubric')); $form->setType('rubric', PARAM_RAW); $buttonarray = array(); $buttonarray[] = &$form->createElement('submit', 'saverubric', get_string('saverubric', 'gradingform_rubric')); if ($this->_customdata['allowdraft']) { $buttonarray[] = &$form->createElement('submit', 'saverubricdraft', get_string('saverubricdraft', 'gradingform_rubric')); } $editbutton = &$form->createElement('submit', 'editrubric', ' '); $editbutton->freeze(); $buttonarray[] = &$editbutton; $buttonarray[] = &$form->createElement('cancel'); $form->addGroup($buttonarray, 'buttonar', '', array(' '), false); $form->closeHeaderBefore('buttonar'); } /** * Setup the form depending on current values. This method is called after definition(), * data submission and set_data(). * All form setup that is dependent on form values should go in here. * * We remove the element status if there is no current status (i.e. rubric is only being created) * so the users do not get confused */ public function definition_after_data() { $form = $this->_form; $el = $form->getElement('status'); if (!$el->getValue()) { $form->removeElement('status'); } else { $vals = array_values($el->getValue()); if ($vals[0] == gradingform_controller::DEFINITION_STATUS_READY) { $this->findButton('saverubric')->setValue(get_string('save', 'gradingform_rubric')); } } } /** * Form vlidation. * If there are errors return array of errors ("fieldname"=>"error message"), * otherwise true if ok. * * @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, * or an empty array if everything is OK (true allowed for backwards compatibility too). */ public function validation($data, $files) { $err = parent::validation($data, $files); $err = array(); $form = $this->_form; $rubricel = $form->getElement('rubric'); if ($rubricel->non_js_button_pressed($data['rubric'])) { // if JS is disabled and button such as 'Add criterion' is pressed - prevent from submit $err['rubricdummy'] = 1; } else if (isset($data['editrubric'])) { // continue editing $err['rubricdummy'] = 1; } else if (isset($data['saverubric']) && $data['saverubric']) { // If user attempts to make rubric active - it needs to be validated if ($rubricel->validate($data['rubric']) !== false) { $err['rubricdummy'] = 1; } } return $err; } /** * Return submitted data if properly submitted or returns NULL if validation fails or * if there is no submitted data. * * @return object submitted data; NULL if not valid or not submitted or cancelled */ public function get_data() { $data = parent::get_data(); if (!empty($data->saverubric)) { $data->status = gradingform_controller::DEFINITION_STATUS_READY; } else if (!empty($data->saverubricdraft)) { $data->status = gradingform_controller::DEFINITION_STATUS_DRAFT; } return $data; } /** * Check if there are changes in the rubric and it is needed to ask user whether to * mark the current grades for re-grading. User may confirm re-grading and continue, * return to editing or cancel the changes * * @param gradingform_rubric_controller $controller */ public function need_confirm_regrading($controller) { $data = $this->get_data(); if (isset($data->rubric['regrade'])) { // we have already displayed the confirmation on the previous step return false; } if (!isset($data->saverubric) || !$data->saverubric) { // we only need confirmation when button 'Save rubric' is pressed return false; } if (!$controller->has_active_instances()) { // nothing to re-grade, confirmation not needed return false; } $changelevel = $controller->update_or_check_rubric($data); if ($changelevel == 0) { // no changes in the rubric, no confirmation needed return false; } // freeze form elements and pass the values in hidden fields // TODO MDL-29421 description_editor does not freeze the normal way, uncomment below when fixed $form = $this->_form; foreach (array('rubric', 'name'/*, 'description_editor'*/) as $fieldname) { $el =& $form->getElement($fieldname); $el->freeze(); $el->setPersistantFreeze(true); if ($fieldname == 'rubric') { $el->add_regrade_confirmation($changelevel); } } // replace button text 'saverubric' and unfreeze 'Back to edit' button $this->findButton('saverubric')->setValue(get_string('continue')); $el =& $this->findButton('editrubric'); $el->setValue(get_string('backtoediting', 'gradingform_rubric')); $el->unfreeze(); return true; } /** * Returns a form element (submit button) with the name $elementname * * @param string $elementname * @return HTML_QuickForm_element */ protected function &findButton($elementname) { $form = $this->_form; $buttonar =& $form->getElement('buttonar'); $elements =& $buttonar->getElements(); foreach ($elements as $el) { if ($el->getName() == $elementname) { return $el; } } return null; } } PK'\OYform/rubric/pix/icon.svgnu[ ]>PK'\4form/rubric/pix/icon.pngnu[PNG  IHDRabKGDHIDAT8 0 /,Th R$k0y 0Rkw_y] /@6̉;W=KIENDB`PK'\_vvform/rubric/lib.phpnu[. /** * Grading method controller for the Rubric plugin * * @package gradingform_rubric * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_external\external_format_value; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/grade/grading/form/lib.php'); require_once($CFG->dirroot.'/lib/filelib.php'); /** rubric: Used to compare our gradeitem_type against. */ const RUBRIC = 'rubric'; /** * This controller encapsulates the rubric grading logic * * @package gradingform_rubric * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradingform_rubric_controller extends gradingform_controller { // Modes of displaying the rubric (used in gradingform_rubric_renderer) /** Rubric display mode: For editing (moderator or teacher creates a rubric) */ const DISPLAY_EDIT_FULL = 1; /** Rubric display mode: Preview the rubric design with hidden fields */ const DISPLAY_EDIT_FROZEN = 2; /** Rubric display mode: Preview the rubric design (for person with manage permission) */ const DISPLAY_PREVIEW = 3; /** Rubric display mode: Preview the rubric (for people being graded) */ const DISPLAY_PREVIEW_GRADED= 8; /** Rubric display mode: For evaluation, enabled (teacher grades a student) */ const DISPLAY_EVAL = 4; /** Rubric display mode: For evaluation, with hidden fields */ const DISPLAY_EVAL_FROZEN = 5; /** Rubric display mode: Teacher reviews filled rubric */ const DISPLAY_REVIEW = 6; /** Rubric display mode: Dispaly filled rubric (i.e. students see their grades) */ const DISPLAY_VIEW = 7; /** * Extends the module settings navigation with the rubric grading settings * * This function is called when the context for the page is an activity module with the * FEATURE_ADVANCED_GRADING, the user has the permission moodle/grade:managegradingforms * and there is an area with the active grading method set to 'rubric'. * * @param settings_navigation $settingsnav {@link settings_navigation} * @param navigation_node $node {@link navigation_node} */ public function extend_settings_navigation(settings_navigation $settingsnav, ?navigation_node $node=null) { $node->add(get_string('definerubric', 'gradingform_rubric'), $this->get_editor_url(), settings_navigation::TYPE_CUSTOM, null, null, new pix_icon('icon', '', 'gradingform_rubric')); } /** * Extends the module navigation * * This function is called when the context for the page is an activity module with the * FEATURE_ADVANCED_GRADING and there is an area with the active grading method set to the given plugin. * * @param global_navigation $navigation {@link global_navigation} * @param navigation_node $node {@link navigation_node} */ public function extend_navigation(global_navigation $navigation, ?navigation_node $node=null) { if (has_capability('moodle/grade:managegradingforms', $this->get_context())) { // no need for preview if user can manage forms, he will have link to manage.php in settings instead return; } if ($this->is_form_defined() && ($options = $this->get_options()) && !empty($options['alwaysshowdefinition'])) { $node->add(get_string('gradingof', 'gradingform_rubric', get_grading_manager($this->get_areaid())->get_area_title()), new moodle_url('/grade/grading/form/'.$this->get_method_name().'/preview.php', array('areaid' => $this->get_areaid())), settings_navigation::TYPE_CUSTOM); } } /** * Saves the rubric definition into the database * * @see parent::update_definition() * @param stdClass $newdefinition rubric definition data as coming from gradingform_rubric_editrubric::get_data() * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user */ public function update_definition(stdClass $newdefinition, $usermodified = null) { $this->update_or_check_rubric($newdefinition, $usermodified, true); if (isset($newdefinition->rubric['regrade']) && $newdefinition->rubric['regrade']) { $this->mark_for_regrade(); } } /** * Either saves the rubric definition into the database or check if it has been changed. * Returns the level of changes: * 0 - no changes * 1 - only texts or criteria sortorders are changed, students probably do not require re-grading * 2 - added levels but maximum score on rubric is the same, students still may not require re-grading * 3 - removed criteria or added levels or changed number of points, students require re-grading but may be re-graded automatically * 4 - removed levels - students require re-grading and not all students may be re-graded automatically * 5 - added criteria - all students require manual re-grading * * @param stdClass $newdefinition rubric definition data as coming from gradingform_rubric_editrubric::get_data() * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user * @param boolean $doupdate if true actually updates DB, otherwise performs a check * */ public function update_or_check_rubric(stdClass $newdefinition, $usermodified = null, $doupdate = false) { global $DB; // firstly update the common definition data in the {grading_definition} table if ($this->definition === false) { if (!$doupdate) { // if we create the new definition there is no such thing as re-grading anyway return 5; } // if definition does not exist yet, create a blank one // (we need id to save files embedded in description) parent::update_definition(new stdClass(), $usermodified); parent::load_definition(); } if (!isset($newdefinition->rubric['options'])) { $newdefinition->rubric['options'] = self::get_default_options(); } $newdefinition->options = json_encode($newdefinition->rubric['options']); $editoroptions = self::description_form_field_options($this->get_context()); $newdefinition = file_postupdate_standard_editor($newdefinition, 'description', $editoroptions, $this->get_context(), 'grading', 'description', $this->definition->id); // reload the definition from the database $currentdefinition = $this->get_definition(true); $haschanges = array(); // Check if 'lockzeropoints' option has changed. $newlockzeropoints = $newdefinition->rubric['options']['lockzeropoints']; $currentoptions = $this->get_options(); if ((bool)$newlockzeropoints != (bool)$currentoptions['lockzeropoints']) { $haschanges[3] = true; } // update rubric data if (empty($newdefinition->rubric['criteria'])) { $newcriteria = array(); } else { $newcriteria = $newdefinition->rubric['criteria']; // new ones to be saved } $currentcriteria = $currentdefinition->rubric_criteria; $criteriafields = array('sortorder', 'description', 'descriptionformat'); $levelfields = array('score', 'definition', 'definitionformat'); foreach ($newcriteria as $id => $criterion) { // get list of submitted levels $levelsdata = array(); if (array_key_exists('levels', $criterion)) { $levelsdata = $criterion['levels']; } $criterionmaxscore = null; if (preg_match('/^NEWID\d+$/', $id)) { // insert criterion into DB $data = array('definitionid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE); // TODO MDL-31235 format is not supported yet foreach ($criteriafields as $key) { if (array_key_exists($key, $criterion)) { $data[$key] = $criterion[$key]; } } if ($doupdate) { $id = $DB->insert_record('gradingform_rubric_criteria', $data); } $haschanges[5] = true; } else { // update criterion in DB $data = array(); foreach ($criteriafields as $key) { if (array_key_exists($key, $criterion) && $criterion[$key] != $currentcriteria[$id][$key]) { $data[$key] = $criterion[$key]; } } if (!empty($data)) { // update only if something is changed $data['id'] = $id; if ($doupdate) { $DB->update_record('gradingform_rubric_criteria', $data); } $haschanges[1] = true; } // remove deleted levels from DB and calculate the maximum score for this criteria foreach ($currentcriteria[$id]['levels'] as $levelid => $currentlevel) { if ($criterionmaxscore === null || $criterionmaxscore < $currentlevel['score']) { $criterionmaxscore = $currentlevel['score']; } if (!array_key_exists($levelid, $levelsdata)) { if ($doupdate) { $DB->delete_records('gradingform_rubric_levels', array('id' => $levelid)); } $haschanges[4] = true; } } } foreach ($levelsdata as $levelid => $level) { if (isset($level['score'])) { $level['score'] = unformat_float($level['score']); } if (preg_match('/^NEWID\d+$/', $levelid)) { // insert level into DB $data = array('criterionid' => $id, 'definitionformat' => FORMAT_MOODLE); // TODO MDL-31235 format is not supported yet foreach ($levelfields as $key) { if (array_key_exists($key, $level)) { $data[$key] = $level[$key]; } } if ($doupdate) { $levelid = $DB->insert_record('gradingform_rubric_levels', $data); } if ($criterionmaxscore !== null && $criterionmaxscore >= $level['score']) { // new level is added but the maximum score for this criteria did not change, re-grading may not be necessary $haschanges[2] = true; } else { $haschanges[3] = true; } } else { // update level in DB $data = array(); foreach ($levelfields as $key) { if (array_key_exists($key, $level) && $level[$key] != $currentcriteria[$id]['levels'][$levelid][$key]) { $data[$key] = $level[$key]; } } if (!empty($data)) { // update only if something is changed $data['id'] = $levelid; if ($doupdate) { $DB->update_record('gradingform_rubric_levels', $data); } if (isset($data['score'])) { $haschanges[3] = true; } $haschanges[1] = true; } } } } // remove deleted criteria from DB foreach (array_keys($currentcriteria) as $id) { if (!array_key_exists($id, $newcriteria)) { if ($doupdate) { $DB->delete_records('gradingform_rubric_criteria', array('id' => $id)); $DB->delete_records('gradingform_rubric_levels', array('criterionid' => $id)); } $haschanges[3] = true; } } foreach (array('status', 'description', 'descriptionformat', 'name', 'options') as $key) { if (isset($newdefinition->$key) && $newdefinition->$key != $this->definition->$key) { $haschanges[1] = true; } } if ($usermodified && $usermodified != $this->definition->usermodified) { $haschanges[1] = true; } if (!count($haschanges)) { return 0; } if ($doupdate) { parent::update_definition($newdefinition, $usermodified); $this->load_definition(); } // return the maximum level of changes $changelevels = array_keys($haschanges); sort($changelevels); return array_pop($changelevels); } /** * Marks all instances filled with this rubric with the status INSTANCE_STATUS_NEEDUPDATE */ public function mark_for_regrade() { global $DB; if ($this->has_active_instances()) { $conditions = array('definitionid' => $this->definition->id, 'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE); $DB->set_field('grading_instances', 'status', gradingform_instance::INSTANCE_STATUS_NEEDUPDATE, $conditions); } } /** * Loads the rubric form definition if it exists * * There is a new array called 'rubric_criteria' appended to the list of parent's definition properties. */ protected function load_definition() { global $DB; $sql = "SELECT gd.*, rc.id AS rcid, rc.sortorder AS rcsortorder, rc.description AS rcdescription, rc.descriptionformat AS rcdescriptionformat, rl.id AS rlid, rl.score AS rlscore, rl.definition AS rldefinition, rl.definitionformat AS rldefinitionformat FROM {grading_definitions} gd LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.definitionid = gd.id) LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id) WHERE gd.areaid = :areaid AND gd.method = :method ORDER BY rc.sortorder,rl.score"; $params = array('areaid' => $this->areaid, 'method' => $this->get_method_name()); $rs = $DB->get_recordset_sql($sql, $params); $this->definition = false; foreach ($rs as $record) { // pick the common definition data if ($this->definition === false) { $this->definition = new stdClass(); foreach (array('id', 'name', 'description', 'descriptionformat', 'status', 'copiedfromid', 'timecreated', 'usercreated', 'timemodified', 'usermodified', 'timecopied', 'options') as $fieldname) { $this->definition->$fieldname = $record->$fieldname; } $this->definition->rubric_criteria = array(); } // pick the criterion data if (!empty($record->rcid) and empty($this->definition->rubric_criteria[$record->rcid])) { foreach (array('id', 'sortorder', 'description', 'descriptionformat') as $fieldname) { $this->definition->rubric_criteria[$record->rcid][$fieldname] = $record->{'rc'.$fieldname}; } $this->definition->rubric_criteria[$record->rcid]['levels'] = array(); } // pick the level data if (!empty($record->rlid)) { foreach (array('id', 'score', 'definition', 'definitionformat') as $fieldname) { $value = $record->{'rl'.$fieldname}; if ($fieldname == 'score') { $value = (float)$value; // To prevent display like 1.00000 } $this->definition->rubric_criteria[$record->rcid]['levels'][$record->rlid][$fieldname] = $value; } } } $rs->close(); $options = $this->get_options(); if (!$options['sortlevelsasc']) { foreach (array_keys($this->definition->rubric_criteria) as $rcid) { $this->definition->rubric_criteria[$rcid]['levels'] = array_reverse($this->definition->rubric_criteria[$rcid]['levels'], true); } } } /** * Returns the default options for the rubric display * * @return array */ public static function get_default_options() { $options = array( 'sortlevelsasc' => 1, 'lockzeropoints' => 1, 'alwaysshowdefinition' => 1, 'showdescriptionteacher' => 1, 'showdescriptionstudent' => 1, 'showscoreteacher' => 1, 'showscorestudent' => 1, 'enableremarks' => 1, 'showremarksstudent' => 1 ); return $options; } /** * Gets the options of this rubric definition, fills the missing options with default values * * The only exception is 'lockzeropoints' - if other options are present in the json string but this * one is absent, this means that the rubric was created before Moodle 3.2 and the 0 value should be used. * * @return array */ public function get_options() { $options = self::get_default_options(); if (!empty($this->definition->options)) { $thisoptions = json_decode($this->definition->options, true); // Assoc. array is expected. foreach ($thisoptions as $option => $value) { $options[$option] = $value; } if (!array_key_exists('lockzeropoints', $thisoptions)) { // Rubrics created before Moodle 3.2 don't have 'lockzeropoints' option. In this case they should not // assume default value 1 but use "legacy" value 0. $options['lockzeropoints'] = 0; } } return $options; } /** * Converts the current definition into an object suitable for the editor form's set_data() * * @param boolean $addemptycriterion whether to add an empty criterion if the rubric is completely empty (just being created) * @return stdClass */ public function get_definition_for_editing($addemptycriterion = false) { $definition = $this->get_definition(); $properties = new stdClass(); $properties->areaid = $this->areaid; if ($definition) { foreach (array('id', 'name', 'description', 'descriptionformat', 'status') as $key) { $properties->$key = $definition->$key; } $options = self::description_form_field_options($this->get_context()); $properties = file_prepare_standard_editor($properties, 'description', $options, $this->get_context(), 'grading', 'description', $definition->id); } $properties->rubric = array('criteria' => array(), 'options' => $this->get_options()); if (!empty($definition->rubric_criteria)) { $properties->rubric['criteria'] = $definition->rubric_criteria; } else if (!$definition && $addemptycriterion) { $properties->rubric['criteria'] = array('addcriterion' => 1); } return $properties; } /** * Returns the form definition suitable for cloning into another area * * @see parent::get_definition_copy() * @param gradingform_controller $target the controller of the new copy * @return stdClass definition structure to pass to the target's {@link update_definition()} */ public function get_definition_copy(gradingform_controller $target) { $new = parent::get_definition_copy($target); $old = $this->get_definition_for_editing(); $new->description_editor = $old->description_editor; $new->rubric = array('criteria' => array(), 'options' => $old->rubric['options']); $newcritid = 1; $newlevid = 1; foreach ($old->rubric['criteria'] as $oldcritid => $oldcrit) { unset($oldcrit['id']); if (isset($oldcrit['levels'])) { foreach ($oldcrit['levels'] as $oldlevid => $oldlev) { unset($oldlev['id']); $oldcrit['levels']['NEWID'.$newlevid] = $oldlev; unset($oldcrit['levels'][$oldlevid]); $newlevid++; } } else { $oldcrit['levels'] = array(); } $new->rubric['criteria']['NEWID'.$newcritid] = $oldcrit; $newcritid++; } return $new; } /** * Options for displaying the rubric description field in the form * * @param object $context * @return array options for the form description field */ public static function description_form_field_options($context) { global $CFG; return array( 'maxfiles' => -1, 'maxbytes' => get_user_max_upload_file_size($context, $CFG->maxbytes), 'context' => $context, ); } /** * Formats the definition description for display on page * * @return string */ public function get_formatted_description() { if (!isset($this->definition->description)) { return ''; } $context = $this->get_context(); $options = self::description_form_field_options($this->get_context()); $description = file_rewrite_pluginfile_urls($this->definition->description, 'pluginfile.php', $context->id, 'grading', 'description', $this->definition->id, $options); $formatoptions = array( 'noclean' => false, 'trusted' => false, 'filter' => true, 'context' => $context ); return format_text($description, $this->definition->descriptionformat, $formatoptions); } /** * Returns the rubric plugin renderer * * @param moodle_page $page the target page * @return gradingform_rubric_renderer */ public function get_renderer(moodle_page $page) { return $page->get_renderer('gradingform_'. $this->get_method_name()); } /** * Returns the HTML code displaying the preview of the grading form * * @param moodle_page $page the target page * @return string */ public function render_preview(moodle_page $page) { if (!$this->is_form_defined()) { throw new coding_exception('It is the caller\'s responsibility to make sure that the form is actually defined'); } $criteria = $this->definition->rubric_criteria; $options = $this->get_options(); $rubric = ''; if (has_capability('moodle/grade:managegradingforms', $page->context)) { $showdescription = true; } else { if (empty($options['alwaysshowdefinition'])) { // ensure we don't display unless show rubric option enabled return ''; } $showdescription = $options['showdescriptionstudent']; } $output = $this->get_renderer($page); if ($showdescription) { $rubric .= $output->box($this->get_formatted_description(), 'gradingform_rubric-description'); } if (has_capability('moodle/grade:managegradingforms', $page->context)) { if (!$options['lockzeropoints']) { // Warn about using grade calculation method where minimum number of points is flexible. $rubric .= $output->display_rubric_mapping_explained($this->get_min_max_score()); } $rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric'); } else { $rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW_GRADED, 'rubric'); } return $rubric; } /** * Deletes the rubric definition and all the associated information */ protected function delete_plugin_definition() { global $DB; // get the list of instances $instances = array_keys($DB->get_records('grading_instances', array('definitionid' => $this->definition->id), '', 'id')); // delete all fillings $DB->delete_records_list('gradingform_rubric_fillings', 'instanceid', $instances); // delete instances $DB->delete_records_list('grading_instances', 'id', $instances); // get the list of criteria records $criteria = array_keys($DB->get_records('gradingform_rubric_criteria', array('definitionid' => $this->definition->id), '', 'id')); // delete levels $DB->delete_records_list('gradingform_rubric_levels', 'criterionid', $criteria); // delete critera $DB->delete_records_list('gradingform_rubric_criteria', 'id', $criteria); } /** * If instanceid is specified and grading instance exists and it is created by this rater for * this item, this instance is returned. * If there exists a draft for this raterid+itemid, take this draft (this is the change from parent) * Otherwise new instance is created for the specified rater and itemid * * @param int $instanceid * @param int $raterid * @param int $itemid * @return gradingform_instance */ public function get_or_create_instance($instanceid, $raterid, $itemid) { global $DB; if ($instanceid && $instance = $DB->get_record('grading_instances', array('id' => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) { return $this->get_instance($instance); } if ($itemid && $raterid) { $params = array('definitionid' => $this->definition->id, 'raterid' => $raterid, 'itemid' => $itemid); if ($rs = $DB->get_records('grading_instances', $params, 'timemodified DESC', '*', 0, 1)) { $record = reset($rs); $currentinstance = $this->get_current_instance($raterid, $itemid); if ($record->status == gradingform_rubric_instance::INSTANCE_STATUS_INCOMPLETE && (!$currentinstance || $record->timemodified > $currentinstance->get_data('timemodified'))) { $record->isrestored = true; return $this->get_instance($record); } } } return $this->create_instance($raterid, $itemid); } /** * Returns html code to be included in student's feedback. * * @param moodle_page $page * @param int $itemid * @param array $gradinginfo result of function grade_get_grades * @param string $defaultcontent default string to be returned if no active grading is found * @param boolean $cangrade whether current user has capability to grade in this context * @return string */ public function render_grade($page, $itemid, $gradinginfo, $defaultcontent, $cangrade) { return $this->get_renderer($page)->display_instances($this->get_active_instances($itemid), $defaultcontent, $cangrade); } // ///// full-text search support ///////////////////////////////////////////// /** * Prepare the part of the search query to append to the FROM statement * * @param string $gdid the alias of grading_definitions.id column used by the caller * @return string */ public static function sql_search_from_tables($gdid) { return " LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.definitionid = $gdid) LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id)"; } /** * Prepare the parts of the SQL WHERE statement to search for the given token * * The returned array cosists of the list of SQL comparions and the list of * respective parameters for the comparisons. The returned chunks will be joined * with other conditions using the OR operator. * * @param string $token token to search for * @return array */ public static function sql_search_where($token) { global $DB; $subsql = array(); $params = array(); // search in rubric criteria description $subsql[] = $DB->sql_like('rc.description', '?', false, false); $params[] = '%'.$DB->sql_like_escape($token).'%'; // search in rubric levels definition $subsql[] = $DB->sql_like('rl.definition', '?', false, false); $params[] = '%'.$DB->sql_like_escape($token).'%'; return array($subsql, $params); } /** * Calculates and returns the possible minimum and maximum score (in points) for this rubric * * @return array */ public function get_min_max_score() { if (!$this->is_form_available()) { return null; } $returnvalue = array('minscore' => 0, 'maxscore' => 0); foreach ($this->get_definition()->rubric_criteria as $id => $criterion) { $scores = array(); foreach ($criterion['levels'] as $level) { $scores[] = $level['score']; } sort($scores); $returnvalue['minscore'] += $scores[0]; $returnvalue['maxscore'] += $scores[sizeof($scores)-1]; } return $returnvalue; } /** * @return array An array containing a single key/value pair with the 'rubric_criteria' external_multiple_structure. * @see gradingform_controller::get_external_definition_details() * @since Moodle 2.5 */ public static function get_external_definition_details() { $rubric_criteria = new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'criterion id', VALUE_OPTIONAL), 'sortorder' => new external_value(PARAM_INT, 'sortorder', VALUE_OPTIONAL), 'description' => new external_value(PARAM_RAW, 'description', VALUE_OPTIONAL), 'descriptionformat' => new external_format_value('description', VALUE_OPTIONAL), 'levels' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'level id', VALUE_OPTIONAL), 'score' => new external_value(PARAM_FLOAT, 'score', VALUE_OPTIONAL), 'definition' => new external_value(PARAM_RAW, 'definition', VALUE_OPTIONAL), 'definitionformat' => new external_format_value('definition', VALUE_OPTIONAL) ) ), 'levels', VALUE_OPTIONAL ) ) ), 'definition details', VALUE_OPTIONAL ); return array('rubric_criteria' => $rubric_criteria); } /** * Returns an array that defines the structure of the rubric's filling. This function is used by * the web service function core_grading_external::get_gradingform_instances(). * * @return An array containing a single key/value pair with the 'criteria' external_multiple_structure * @see gradingform_controller::get_external_instance_filling_details() * @since Moodle 2.6 */ public static function get_external_instance_filling_details() { $criteria = new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'filling id'), 'criterionid' => new external_value(PARAM_INT, 'criterion id'), 'levelid' => new external_value(PARAM_INT, 'level id', VALUE_OPTIONAL), 'remark' => new external_value(PARAM_RAW, 'remark', VALUE_OPTIONAL), 'remarkformat' => new external_format_value('remark', VALUE_OPTIONAL) ) ), 'filling', VALUE_OPTIONAL ); return array ('criteria' => $criteria); } } /** * Class to manage one rubric grading instance. * * Stores information and performs actions like update, copy, validate, submit, etc. * * @package gradingform_rubric * @copyright 2011 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradingform_rubric_instance extends gradingform_instance { /** @var array stores the rubric, has two keys: 'criteria' and 'options' */ protected $rubric; /** * Deletes this (INCOMPLETE) instance from database. */ public function cancel() { global $DB; parent::cancel(); $DB->delete_records('gradingform_rubric_fillings', array('instanceid' => $this->get_id())); } /** * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with * the specified values) * * @param int $raterid value for raterid in the duplicate * @param int $itemid value for itemid in the duplicate * @return int id of the new instance */ public function copy($raterid, $itemid) { global $DB; $instanceid = parent::copy($raterid, $itemid); $currentgrade = $this->get_rubric_filling(); foreach ($currentgrade['criteria'] as $criterionid => $record) { $params = array('instanceid' => $instanceid, 'criterionid' => $criterionid, 'levelid' => $record['levelid'], 'remark' => $record['remark'], 'remarkformat' => $record['remarkformat']); $DB->insert_record('gradingform_rubric_fillings', $params); } return $instanceid; } /** * Determines whether the submitted form was empty. * * @param array $elementvalue value of element submitted from the form * @return boolean true if the form is empty */ public function is_empty_form($elementvalue) { $criteria = $this->get_controller()->get_definition()->rubric_criteria; foreach ($criteria as $id => $criterion) { if (isset($elementvalue['criteria'][$id]['levelid']) || !empty($elementvalue['criteria'][$id]['remark'])) { return false; } } return true; } /** * Removes the attempt from the gradingform_guide_fillings table * @param array $data the attempt data */ public function clear_attempt($data) { global $DB; foreach ($data['criteria'] as $criterionid => $record) { $DB->delete_records('gradingform_rubric_fillings', array('criterionid' => $criterionid, 'instanceid' => $this->get_id())); } } /** * Validates that rubric is fully completed and contains valid grade on each criterion * * @param array $elementvalue value of element as came in form submit * @return boolean true if the form data is validated and contains no errors */ public function validate_grading_element($elementvalue) { $criteria = $this->get_controller()->get_definition()->rubric_criteria; if (!isset($elementvalue['criteria']) || !is_array($elementvalue['criteria']) || sizeof($elementvalue['criteria']) < sizeof($criteria)) { return false; } foreach ($criteria as $id => $criterion) { if (!isset($elementvalue['criteria'][$id]['levelid']) || !array_key_exists($elementvalue['criteria'][$id]['levelid'], $criterion['levels'])) { return false; } } return true; } /** * Retrieves from DB and returns the data how this rubric was filled * * @param boolean $force whether to force DB query even if the data is cached * @return array */ public function get_rubric_filling($force = false) { global $DB; if ($this->rubric === null || $force) { $records = $DB->get_records('gradingform_rubric_fillings', array('instanceid' => $this->get_id())); $this->rubric = array('criteria' => array()); foreach ($records as $record) { $this->rubric['criteria'][$record->criterionid] = (array)$record; } } return $this->rubric; } /** * Updates the instance with the data received from grading form. This function may be * called via AJAX when grading is not yet completed, so it does not change the * status of the instance. * * @param array $data */ public function update($data) { global $DB; $currentgrade = $this->get_rubric_filling(); parent::update($data); foreach ($data['criteria'] as $criterionid => $record) { // Hardcoding/defaulting to html format for new/existing record $record['remarkformat'] = FORMAT_HTML; if (!array_key_exists($criterionid, $currentgrade['criteria'])) { $newrecord = array('instanceid' => $this->get_id(), 'criterionid' => $criterionid, 'levelid' => $record['levelid'], 'remarkformat' => $record['remarkformat']); if (isset($record['remark'])) { $newrecord['remark'] = $record['remark']; } $DB->insert_record('gradingform_rubric_fillings', $newrecord); } else { $newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']); foreach (array('levelid', 'remark', 'remarkformat') as $key) { // TODO MDL-31235 format is not supported yet if (isset($record[$key]) && $currentgrade['criteria'][$criterionid][$key] != $record[$key]) { $newrecord[$key] = $record[$key]; } } if (count($newrecord) > 1) { $DB->update_record('gradingform_rubric_fillings', $newrecord); } } } foreach ($currentgrade['criteria'] as $criterionid => $record) { if (!array_key_exists($criterionid, $data['criteria'])) { $DB->delete_records('gradingform_rubric_fillings', array('id' => $record['id'])); } } $this->get_rubric_filling(true); } /** * Calculates the grade to be pushed to the gradebook * * @return float|int the valid grade from $this->get_controller()->get_grade_range() */ public function get_grade() { $grade = $this->get_rubric_filling(); if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) { return -1; } $graderange = array_keys($this->get_controller()->get_grade_range()); if (empty($graderange)) { return -1; } sort($graderange); $mingrade = $graderange[0]; $maxgrade = $graderange[sizeof($graderange) - 1]; $curscore = 0; foreach ($grade['criteria'] as $id => $record) { $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score']; } $allowdecimals = $this->get_controller()->get_allow_grade_decimals(); $options = $this->get_controller()->get_options(); if ($options['lockzeropoints']) { // Grade calculation method when 0-level is locked. $grade = max($mingrade, $curscore / $scores['maxscore'] * $maxgrade); return $allowdecimals ? $grade : round($grade, 0); } else { // Alternative grade calculation method. $gradeoffset = ($curscore - $scores['minscore']) / ($scores['maxscore'] - $scores['minscore']) * ($maxgrade - $mingrade); return ($allowdecimals ? $gradeoffset : round($gradeoffset, 0)) + $mingrade; } } /** * Returns html for form element of type 'grading'. * * @param moodle_page $page * @param MoodleQuickForm_grading $gradingformelement * @return string */ public function render_grading_element($page, $gradingformelement) { global $USER; if (!$gradingformelement->_flagFrozen) { $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js'); $page->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName())), true, $module); $mode = gradingform_rubric_controller::DISPLAY_EVAL; } else { if ($gradingformelement->_persistantFreeze) { $mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN; } else { $mode = gradingform_rubric_controller::DISPLAY_REVIEW; } } $criteria = $this->get_controller()->get_definition()->rubric_criteria; $options = $this->get_controller()->get_options(); $value = $gradingformelement->getValue(); $html = ''; if ($value === null) { $value = $this->get_rubric_filling(); } else if (!$this->validate_grading_element($value)) { $html .= html_writer::tag('div', get_string('rubricnotcompleted', 'gradingform_rubric'), array('class' => 'gradingform_rubric-error')); } $currentinstance = $this->get_current_instance(); if ($currentinstance && $currentinstance->get_status() == gradingform_instance::INSTANCE_STATUS_NEEDUPDATE) { $html .= html_writer::div(get_string('needregrademessage', 'gradingform_rubric'), 'gradingform_rubric-regrade', array('role' => 'alert')); } $haschanges = false; if ($currentinstance) { $curfilling = $currentinstance->get_rubric_filling(); foreach ($curfilling['criteria'] as $criterionid => $curvalues) { $value['criteria'][$criterionid]['savedlevelid'] = $curvalues['levelid']; $newremark = null; $newlevelid = null; if (isset($value['criteria'][$criterionid]['remark'])) $newremark = $value['criteria'][$criterionid]['remark']; if (isset($value['criteria'][$criterionid]['levelid'])) $newlevelid = $value['criteria'][$criterionid]['levelid']; if ($newlevelid != $curvalues['levelid'] || $newremark != $curvalues['remark']) { $haschanges = true; } } } if ($this->get_data('isrestored') && $haschanges) { $html .= html_writer::tag('div', get_string('restoredfromdraft', 'gradingform_rubric'), array('class' => 'gradingform_rubric-restored')); } if (!empty($options['showdescriptionteacher'])) { $html .= html_writer::tag('div', $this->get_controller()->get_formatted_description(), array('class' => 'gradingform_rubric-description')); } $html .= $this->get_controller()->get_renderer($page)->display_rubric($criteria, $options, $mode, $gradingformelement->getName(), $value); return $html; } } PK'\'PPform/rubric/version.phpnu[. /** * Version information for plugin gradingform_rubric * * @package gradingform_rubric * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'gradingform_rubric'; $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->maturity = MATURITY_STABLE; PK'\u"6"6Aform/rubric/classes/grades/grader/gradingpanel/external/fetch.phpnu[. /** * Web services relating to fetching of a rubric for the grading panel. * * @package gradingform_rubric * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types = 1); namespace gradingform_rubric\grades\grader\gradingpanel\external; global $CFG; use coding_exception; use context; use core_grades\component_gradeitem as gradeitem; use core_grades\component_gradeitems; use core_external\external_api; 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 stdClass; use moodle_exception; require_once($CFG->dirroot.'/grade/grading/form/rubric/lib.php'); /** * Web services relating to fetching of a rubric for the grading panel. * * @package gradingform_rubric * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class fetch extends external_api { /** * Describes the parameters for fetching the grading panel for a simple grade. * * @return external_function_parameters * @since Moodle 3.8 */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters ([ 'component' => new external_value( PARAM_ALPHANUMEXT, 'The name of the component', VALUE_REQUIRED ), 'contextid' => new external_value( PARAM_INT, 'The ID of the context being graded', VALUE_REQUIRED ), 'itemname' => new external_value( PARAM_ALPHANUM, 'The grade item itemname being graded', VALUE_REQUIRED ), 'gradeduserid' => new external_value( PARAM_INT, 'The ID of the user show', VALUE_REQUIRED ), ]); } /** * Fetch the data required to build a grading panel for a simple grade. * * @param string $component * @param int $contextid * @param string $itemname * @param int $gradeduserid * @return array * @since Moodle 3.8 */ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid): array { global $CFG, $USER; require_once("{$CFG->libdir}/gradelib.php"); [ 'component' => $component, 'contextid' => $contextid, 'itemname' => $itemname, 'gradeduserid' => $gradeduserid, ] = self::validate_parameters(self::execute_parameters(), [ 'component' => $component, 'contextid' => $contextid, 'itemname' => $itemname, 'gradeduserid' => $gradeduserid, ]); // Validate the context. $context = context::instance_by_id($contextid); self::validate_context($context); // Validate that the supplied itemname is a gradable item. if (!component_gradeitems::is_valid_itemname($component, $itemname)) { throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component"); } // Fetch the gradeitem instance. $gradeitem = gradeitem::instance($component, $context, $itemname); if (RUBRIC !== $gradeitem->get_advanced_grading_method()) { throw new moodle_exception( "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a rubric" ); } // Fetch the actual data. $gradeduser = \core_user::get_user($gradeduserid, '*', MUST_EXIST); // One can access its own grades. Others just if they're graders. if ($gradeduserid != $USER->id) { $gradeitem->require_user_can_grade($gradeduser, $USER); } return self::get_fetch_data($gradeitem, $gradeduser); } /** * Get the data to be fetched and create the structure ready for Mustache. * * @param gradeitem $gradeitem * @param stdClass $gradeduser * @return array */ public static function get_fetch_data(gradeitem $gradeitem, stdClass $gradeduser): array { global $USER; // Set up all the controllers etc that we'll be needing. $hasgrade = $gradeitem->user_has_grade($gradeduser); $grade = $gradeitem->get_formatted_grade_for_user($gradeduser, $USER); $instance = $gradeitem->get_advanced_grading_instance($USER, $grade); if (!$instance) { throw new moodle_exception('error:gradingunavailable', 'grading'); } $controller = $instance->get_controller(); $definition = $controller->get_definition(); $fillings = $instance->get_rubric_filling(); $context = $controller->get_context(); $definitionid = (int) $definition->id; // Set up some items we need to return on other interfaces. $gradegrade = \grade_grade::fetch(['itemid' => $gradeitem->get_grade_item()->id, 'userid' => $gradeduser->id]); $gradername = $gradegrade ? fullname(\core_user::get_user($gradegrade->usermodified)) : null; $maxgrade = max(array_keys($controller->get_grade_range())); $teacherdescription = self::get_formatted_text( $context, $definitionid, 'description', $definition->description, (int) $definition->descriptionformat ); $criterion = []; if ($definition->rubric_criteria) { // Iterate over the defined criterion in the rubric and map out what we need to render each item. $criterion = array_map(function($criterion) use ($definitionid, $fillings, $context, $hasgrade) { // The general structure we'll be returning, we still need to get the remark (if any) and the levels associated. $result = [ 'id' => $criterion['id'], 'description' => self::get_formatted_text( $context, $definitionid, 'description', $criterion['description'], (int) $criterion['descriptionformat'] ), ]; // Do we have an existing grade filling? if so lets get the remark associated to this criteria. $filling = []; if (array_key_exists($criterion['id'], $fillings['criteria'])) { $filling = $fillings['criteria'][$criterion['id']]; $result['remark'] = self::get_formatted_text($context, $definitionid, 'remark', $filling['remark'], (int) FORMAT_HTML ); } // Lets build the levels within a criteria and figure out what needs to go where. $result['levels'] = array_map(function($level) use ($criterion, $filling, $context, $definitionid) { // The bulk of what'll be returned can be defined easily we'll add to this further down. $result = [ 'id' => $level['id'], 'criterionid' => $criterion['id'], 'score' => $level['score'], 'definition' => self::get_formatted_text( $context, $definitionid, 'definition', $level['definition'], (int) $level['definitionformat'] ), 'checked' => null, ]; // Consult the grade filling to see if a level has been selected and if it is the current level. if (array_key_exists('levelid', $filling) && $filling['levelid'] == $level['id']) { $result['checked'] = true; } return $result; }, $criterion['levels']); $nulllevel = [ 'id' => null, 'criterionid' => $criterion['id'], 'score' => '-', 'definition' => get_string('notset', 'gradingform_rubric'), 'checked' => !$hasgrade, ]; // Consult the grade filling to see if a level has been selected and if it is the current level. if (array_key_exists('levelid', $filling) && $filling['levelid'] == 0) { $nulllevel['checked'] = true; } array_unshift($result['levels'], $nulllevel); return $result; }, $definition->rubric_criteria); } return [ 'templatename' => 'gradingform_rubric/grades/grader/gradingpanel', 'hasgrade' => $hasgrade, 'grade' => [ 'instanceid' => $instance->get_id(), 'criteria' => $criterion, 'rubricmode' => 'evaluate editable', 'teacherdescription' => $teacherdescription, 'canedit' => false, 'usergrade' => $grade->usergrade, 'maxgrade' => $maxgrade, 'gradedby' => $gradername, 'timecreated' => $grade->timecreated, 'timemodified' => $grade->timemodified, ], 'warnings' => [], ]; } /** * Describes the data returned from the external function. * * @return external_single_structure * @since Moodle 3.8 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'templatename' => new external_value(PARAM_SAFEPATH, 'The template to use when rendering this data'), 'hasgrade' => new external_value(PARAM_BOOL, 'Does the user have a grade?'), 'grade' => new external_single_structure([ 'instanceid' => new external_value(PARAM_INT, 'The id of the current grading instance'), 'rubricmode' => new external_value(PARAM_RAW, 'The mode i.e. evaluate editable'), 'canedit' => new external_value(PARAM_BOOL, 'Can the user edit this'), 'criteria' => new external_multiple_structure( new external_single_structure([ 'id' => new external_value(PARAM_INT, 'ID of the Criteria'), 'description' => new external_value(PARAM_RAW, 'Description of the Criteria'), 'remark' => new external_value(PARAM_RAW, 'Any remarks for this criterion for the user being assessed', VALUE_OPTIONAL), 'levels' => new external_multiple_structure(new external_single_structure([ 'id' => new external_value(PARAM_INT, 'ID of level'), 'criterionid' => new external_value(PARAM_INT, 'ID of the criterion this matches to'), 'score' => new external_value(PARAM_RAW, 'What this level is worth'), 'definition' => new external_value(PARAM_RAW, 'Definition of the level'), 'checked' => new external_value(PARAM_BOOL, 'Selected flag'), ])), ]) ), 'timecreated' => new external_value(PARAM_INT, 'The time that the grade was created'), 'usergrade' => new external_value(PARAM_RAW, 'Current user grade'), 'maxgrade' => new external_value(PARAM_RAW, 'Max possible grade'), 'gradedby' => new external_value(PARAM_RAW, 'The assumed grader of this grading instance'), 'timemodified' => new external_value(PARAM_INT, 'The time that the grade was last updated'), ]), 'warnings' => new external_warnings(), ]); } /** * Get a formatted version of the remark/description/etc. * * @param context $context * @param int $definitionid * @param string $filearea The file area of the field * @param string $text The text to be formatted * @param int $format The input format of the string * @return string */ protected static function get_formatted_text(context $context, int $definitionid, string $filearea, string $text, int $format): string { $formatoptions = [ 'noclean' => false, 'trusted' => false, 'filter' => true, ]; [$newtext] = \core_external\util::format_text( $text, $format, $context, 'grading', $filearea, $definitionid, $formatoptions ); return $newtext; } } PK'\LAform/rubric/classes/grades/grader/gradingpanel/external/store.phpnu[. /** * Web services relating to fetching of a rubric for the grading panel. * * @package gradingform_rubric * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types = 1); namespace gradingform_rubric\grades\grader\gradingpanel\external; global $CFG; use coding_exception; use context; use core_grades\component_gradeitem as gradeitem; use core_grades\component_gradeitems; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use moodle_exception; require_once($CFG->dirroot.'/grade/grading/form/rubric/lib.php'); /** * Web services relating to storing of a rubric for the grading panel. * * @package gradingform_rubric * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class store extends external_api { /** * Describes the parameters for storing the grading panel for a simple grade. * * @return external_function_parameters * @since Moodle 3.8 */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters ([ 'component' => new external_value( PARAM_ALPHANUMEXT, 'The name of the component', VALUE_REQUIRED ), 'contextid' => new external_value( PARAM_INT, 'The ID of the context being graded', VALUE_REQUIRED ), 'itemname' => new external_value( PARAM_ALPHANUM, 'The grade item itemname being graded', VALUE_REQUIRED ), 'gradeduserid' => new external_value( PARAM_INT, 'The ID of the user show', VALUE_REQUIRED ), 'notifyuser' => new external_value( PARAM_BOOL, 'Wheteher to notify the user or not', VALUE_DEFAULT, false ), 'formdata' => new external_value( PARAM_RAW, 'The serialised form data representing the grade', VALUE_REQUIRED ), ]); } /** * Fetch the data required to build a grading panel for a simple grade. * * @param string $component * @param int $contextid * @param string $itemname * @param int $gradeduserid * @param string $formdata * @param bool $notifyuser * @return array * @throws coding_exception * @throws moodle_exception * @since Moodle 3.8 */ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid, bool $notifyuser, string $formdata): array { global $USER; [ 'component' => $component, 'contextid' => $contextid, 'itemname' => $itemname, 'gradeduserid' => $gradeduserid, 'notifyuser' => $notifyuser, 'formdata' => $formdata, ] = self::validate_parameters(self::execute_parameters(), [ 'component' => $component, 'contextid' => $contextid, 'itemname' => $itemname, 'gradeduserid' => $gradeduserid, 'notifyuser' => $notifyuser, 'formdata' => $formdata, ]); // Validate the context. $context = context::instance_by_id($contextid); self::validate_context($context); // Validate that the supplied itemname is a gradable item. if (!component_gradeitems::is_valid_itemname($component, $itemname)) { throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component"); } // Fetch the gradeitem instance. $gradeitem = gradeitem::instance($component, $context, $itemname); // Validate that this gradeitem is actually enabled. if (!$gradeitem->is_grading_enabled()) { throw new moodle_exception("Grading is not enabled for {$itemname} in this context"); } // Fetch the record for the graded user. $gradeduser = \core_user::get_user($gradeduserid); // Require that this user can save grades. $gradeitem->require_user_can_grade($gradeduser, $USER); if (RUBRIC !== $gradeitem->get_advanced_grading_method()) { throw new moodle_exception( "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a rubric" ); } // Parse the serialised string into an object. $data = []; parse_str($formdata, $data); // Grade. $gradeitem->store_grade_from_formdata($gradeduser, $USER, (object) $data); // Notify. if ($notifyuser) { // Send notification. $gradeitem->send_student_notification($gradeduser, $USER); } // Fetch the updated grade back out. $grade = $gradeitem->get_grade_for_user($gradeduser, $USER); return fetch::get_fetch_data($gradeitem, $gradeduser); } /** * Describes the data returned from the external function. * * @return external_single_structure * @since Moodle 3.8 */ public static function execute_returns(): external_single_structure { return fetch::execute_returns(); } } PK'\. (form/rubric/classes/privacy/provider.phpnu[. /** * Privacy class for requesting user data. * * @package gradingform_rubric * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace gradingform_rubric\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; /** * Privacy class for requesting user data. * * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_grading\privacy\gradingform_provider_v2 { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_database_table('gradingform_rubric_fillings', [ 'instanceid' => 'privacy:metadata:instanceid', 'criterionid' => 'privacy:metadata:criterionid', 'levelid' => 'privacy:metadata:levelid', 'remark' => 'privacy:metadata:remark' ], 'privacy:metadata:fillingssummary'); return $collection; } /** * Export user data relating to an instance ID. * * @param \context $context Context to use with the export writer. * @param int $instanceid The instance ID to export data for. * @param array $subcontext The directory to export this data to. */ public static function export_gradingform_instance_data(\context $context, int $instanceid, array $subcontext) { global $DB; // Get records from the provided params. $params = ['instanceid' => $instanceid]; $sql = "SELECT rc.description, rl.definition, rl.score, rf.remark FROM {gradingform_rubric_fillings} rf JOIN {gradingform_rubric_criteria} rc ON rc.id = rf.criterionid JOIN {gradingform_rubric_levels} rl ON rf.levelid = rl.id WHERE rf.instanceid = :instanceid"; $records = $DB->get_records_sql($sql, $params); if ($records) { $subcontext = array_merge($subcontext, [get_string('rubric', 'gradingform_rubric'), $instanceid]); \core_privacy\local\request\writer::with_context($context)->export_data($subcontext, (object) $records); } } /** * Deletes all user data related to the provided instance IDs. * * @param array $instanceids The instance IDs to delete information from. */ public static function delete_gradingform_for_instances(array $instanceids) { global $DB; $DB->delete_records_list('gradingform_rubric_fillings', 'instanceid', $instanceids); } } PK'\79form/rubric/templates/grades/grader/gradingpanel.mustachenu[{{! 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 gradingform_rubric/grades/grader/gradingpanel Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * instanceid: Instance of the module this grading form belongs too * criteria: A gradeable item in the Marking Guide * id: The ID of the criteria * description: Description of the criteria * levels: The level that a criteria can be graded at * criterionid: The ID of the criteria * checked: Flag for if this is the currently selected level * definition: Definition of the level * remark: Text input for the teacher to relay to the student Example context (json): { "instanceid": "42", "criteria": [ { "id": 13, "description": "Show your motivation to rock climbing", "levels": [ { "criterionid": 13, "checked": true, "definition": "Great work!" } ], "remark": "That's great!" } ] } }}
{{#criteria}}
{{{description}}}
{{#levels}}
{{/levels}}
{{/criteria}}
{{#js}} require(['core/auto_rows'], function(AutoRows) { AutoRows.init(document.getElementById('gradingform_rubric-{{uniqid}}')); }); {{/js}} PK'\fooform/rubric/preview.phpnu[. /** * Preview rubric page * * @package gradingform_rubric * @copyright 2011 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__.'/../../../../config.php'); require_once(__DIR__.'/lib.php'); require_once(__DIR__.'/edit_form.php'); require_once($CFG->dirroot.'/grade/grading/lib.php'); $areaid = required_param('areaid', PARAM_INT); $manager = get_grading_manager($areaid); list($context, $course, $cm) = get_context_info_array($manager->get_context()->id); require_login($course, true, $cm); $controller = $manager->get_controller('rubric'); $options = $controller->get_options(); if (!$controller->is_form_defined() || empty($options['alwaysshowdefinition'])) { throw new moodle_exception('nopermissions', 'error', '', get_string('previewrubric', 'gradingform_rubric')); } $title = get_string('gradingof', 'gradingform_rubric', $manager->get_area_title()); $PAGE->set_url(new moodle_url('/grade/grading/form/rubric/preview.php', array('areaid' => $areaid))); $PAGE->set_title($title); $PAGE->set_heading($title); echo $OUTPUT->header(); echo $OUTPUT->heading($title); echo $controller->render_preview($PAGE); echo $OUTPUT->footer(); PK'\$z form/rubric/edit.phpnu[. /** * Rubric editor page * * @package gradingform_rubric * @copyright 2011 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__.'/../../../../config.php'); require_once(__DIR__.'/lib.php'); require_once(__DIR__.'/edit_form.php'); require_once($CFG->dirroot.'/grade/grading/lib.php'); $areaid = required_param('areaid', PARAM_INT); $manager = get_grading_manager($areaid); list($context, $course, $cm) = get_context_info_array($manager->get_context()->id); require_login($course, true, $cm); require_capability('moodle/grade:managegradingforms', $context); $controller = $manager->get_controller('rubric'); $PAGE->set_url(new moodle_url('/grade/grading/form/rubric/edit.php', array('areaid' => $areaid))); $PAGE->set_title(get_string('definerubric', 'gradingform_rubric')); $PAGE->set_heading(get_string('definerubric', 'gradingform_rubric')); $mform = new gradingform_rubric_editrubric(null, array('areaid' => $areaid, 'context' => $context, 'allowdraft' => !$controller->has_active_instances()), 'post', '', array('class' => 'gradingform_rubric_editform')); $data = $controller->get_definition_for_editing(true); $returnurl = optional_param('returnurl', $manager->get_management_url(), PARAM_LOCALURL); $data->returnurl = $returnurl; $mform->set_data($data); if ($mform->is_cancelled()) { redirect($returnurl); } else if ($mform->is_submitted() && $mform->is_validated() && !$mform->need_confirm_regrading($controller)) { // Everything ok, validated, re-grading confirmed if needed. Make changes to the rubric. $data = $mform->get_data(); $controller->update_definition($data); // If we do not go back to management url and the minscore warning needs to be displayed, display it during redirection. $warning = null; if (!empty($data->returnurl) && $data->returnurl !== $manager->get_management_url()->out(false)) { if (empty($data->rubric['options']['lockzeropoints']) && ($scores = $controller->get_min_max_score()) && $scores['minscore'] <> 0) { $warning = get_string('zerolevelsabsent', 'gradingform_rubric').'
'. html_writer::link($manager->get_management_url(), get_string('back')); } } redirect($returnurl, $warning, null, \core\output\notification::NOTIFY_ERROR); } // Try to keep the session alive on this page as it may take some time // before significant interaction happens with the server. \core\session\manager::keepalive(); echo $OUTPUT->header(); $mform->display(); echo $OUTPUT->footer(); PK'\Qh@@form/rubric/rubriceditor.phpnu[. /** * File contains definition of class MoodleQuickForm_rubriceditor * * @package gradingform_rubric * @copyright 2011 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once("HTML/QuickForm/input.php"); /** * Form element for handling rubric editor * * The rubric editor is defined as a separate form element. This allows us to render * criteria, levels and buttons using the rubric's own renderer. Also, the required * Javascript library is included, which processes, on the client, buttons needed * for reordering, adding and deleting criteria. * * If Javascript is disabled when one of those special buttons is pressed, the form * element is not validated and, instead of submitting the form, we process button presses. * * @package gradingform_rubric * @copyright 2011 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input { /** @var string help message */ public $_helpbutton = ''; /** @var string|bool stores the result of the last validation: null - undefined, false - no errors, string - error(s) text */ protected $validationerrors = null; /** @var bool if element has already been validated **/ protected $wasvalidated = false; /** @var bool If non-submit (JS) button was pressed: null - unknown, true/false - button was/wasn't pressed */ protected $nonjsbuttonpressed = false; /** @var bool Message to display in front of the editor (that there exist grades on this rubric being edited) */ protected $regradeconfirmation = false; /** * Constructor for rubric editor * * @param string $elementName * @param string $elementLabel * @param array $attributes */ public function __construct($elementName=null, $elementLabel=null, $attributes=null) { parent::__construct($elementName, $elementLabel, $attributes); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function MoodleQuickForm_rubriceditor($elementName=null, $elementLabel=null, $attributes=null) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($elementName, $elementLabel, $attributes); } /** * get html for help button * * @return string html for help button */ public function getHelpButton() { return $this->_helpbutton; } /** * The renderer will take care itself about different display in normal and frozen states * * @return string */ public function getElementTemplateType() { return 'default'; } /** * Specifies that confirmation about re-grading needs to be added to this rubric editor. * $changelevel is saved in $this->regradeconfirmation and retrieved in toHtml() * * @see gradingform_rubric_controller::update_or_check_rubric() * @param int $changelevel */ public function add_regrade_confirmation($changelevel) { $this->regradeconfirmation = $changelevel; } /** * Returns html string to display this element * * @return string */ public function toHtml() { global $PAGE; $html = $this->_getTabs(); $renderer = $PAGE->get_renderer('gradingform_rubric'); $data = $this->prepare_data(null, $this->wasvalidated); if (!$this->_flagFrozen) { $mode = gradingform_rubric_controller::DISPLAY_EDIT_FULL; $module = array('name'=>'gradingform_rubriceditor', 'fullpath'=>'/grade/grading/form/rubric/js/rubriceditor.js', 'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'), 'strings' => array(array('confirmdeletecriterion', 'gradingform_rubric'), array('confirmdeletelevel', 'gradingform_rubric'), array('criterionempty', 'gradingform_rubric'), array('levelempty', 'gradingform_rubric') )); $PAGE->requires->js_init_call('M.gradingform_rubriceditor.init', array( array('name' => $this->getName(), 'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()), 'leveltemplate' => $renderer->level_template($mode, $data['options'], $this->getName()) )), true, $module); } else { // Rubric is frozen, no javascript needed if ($this->_persistantFreeze) { $mode = gradingform_rubric_controller::DISPLAY_EDIT_FROZEN; } else { $mode = gradingform_rubric_controller::DISPLAY_PREVIEW; } } if ($this->regradeconfirmation) { if (!isset($data['regrade'])) { $data['regrade'] = 1; } $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']); } if ($this->validationerrors) { $html .= html_writer::div($renderer->notification($this->validationerrors)); } $html .= $renderer->display_rubric($data['criteria'], $data['options'], $mode, $this->getName()); return $html; } /** * Prepares the data passed in $_POST: * - processes the pressed buttons 'addlevel', 'addcriterion', 'moveup', 'movedown', 'delete' (when JavaScript is disabled) * sets $this->nonjsbuttonpressed to true/false if such button was pressed * - if options not passed (i.e. we create a new rubric) fills the options array with the default values * - if options are passed completes the options array with unchecked checkboxes * - if $withvalidation is set, adds 'error_xxx' attributes to elements that contain errors and creates an error string * and stores it in $this->validationerrors * * @param array $value * @param boolean $withvalidation whether to enable data validation * @return array */ protected function prepare_data($value = null, $withvalidation = false) { if (null === $value) { $value = $this->getValue(); } if ($this->nonjsbuttonpressed === null) { $this->nonjsbuttonpressed = false; } $totalscore = 0; $errors = array(); $return = array('criteria' => array(), 'options' => gradingform_rubric_controller::get_default_options()); if (!isset($value['criteria'])) { $value['criteria'] = array(); $errors['err_nocriteria'] = 1; } // If options are present in $value, replace default values with submitted values if (!empty($value['options'])) { foreach (array_keys($return['options']) as $option) { // special treatment for checkboxes if (!empty($value['options'][$option])) { $return['options'][$option] = $value['options'][$option]; } else { $return['options'][$option] = null; } } } if (is_array($value)) { // for other array keys of $value no special treatmeant neeeded, copy them to return value as is foreach (array_keys($value) as $key) { if ($key != 'options' && $key != 'criteria') { $return[$key] = $value[$key]; } } } // iterate through criteria $lastaction = null; $lastid = null; $overallminscore = $overallmaxscore = 0; foreach ($value['criteria'] as $id => $criterion) { if ($id == 'addcriterion') { $id = $this->get_next_id(array_keys($value['criteria'])); $criterion = array('description' => '', 'levels' => array()); $i = 0; // when adding new criterion copy the number of levels and their scores from the last criterion if (!empty($value['criteria'][$lastid]['levels'])) { foreach ($value['criteria'][$lastid]['levels'] as $lastlevel) { $criterion['levels']['NEWID'.($i++)]['score'] = $lastlevel['score']; } } else { $criterion['levels']['NEWID'.($i++)]['score'] = 0; } // add more levels so there are at least 3 in the new criterion. Increment by 1 the score for each next one for ($i=$i; $i<3; $i++) { $criterion['levels']['NEWID'.$i]['score'] = $criterion['levels']['NEWID'.($i-1)]['score'] + 1; } // set other necessary fields (definition) for the levels in the new criterion foreach (array_keys($criterion['levels']) as $i) { $criterion['levels'][$i]['definition'] = ''; } $this->nonjsbuttonpressed = true; } $levels = array(); $minscore = $maxscore = null; if (array_key_exists('levels', $criterion)) { foreach ($criterion['levels'] as $levelid => $level) { if ($levelid == 'addlevel') { $levelid = $this->get_next_id(array_keys($criterion['levels'])); $level = array( 'definition' => '', 'score' => 0, ); foreach ($criterion['levels'] as $lastlevel) { if (isset($lastlevel['score'])) { $level['score'] = max($level['score'], ceil(unformat_float($lastlevel['score'])) + 1); } } $this->nonjsbuttonpressed = true; } if (!array_key_exists('delete', $level)) { $score = unformat_float($level['score'], true); if ($withvalidation) { if (!strlen(trim($level['definition']))) { $errors['err_nodefinition'] = 1; $level['error_definition'] = true; } if ($score === null || $score === false) { $errors['err_scoreformat'] = 1; $level['error_score'] = true; } } $levels[$levelid] = $level; if ($minscore === null || $score < $minscore) { $minscore = $score; } if ($maxscore === null || $score > $maxscore) { $maxscore = $score; } } else { $this->nonjsbuttonpressed = true; } } } $totalscore += (float)$maxscore; $criterion['levels'] = $levels; if ($withvalidation && !array_key_exists('delete', $criterion)) { if (count($levels)<2) { $errors['err_mintwolevels'] = 1; $criterion['error_levels'] = true; } if (!strlen(trim($criterion['description']))) { $errors['err_nodescription'] = 1; $criterion['error_description'] = true; } $overallmaxscore += $maxscore; $overallminscore += $minscore; } if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') { unset($criterion['moveup']); if ($lastid !== null) { $lastcriterion = $return['criteria'][$lastid]; unset($return['criteria'][$lastid]); $return['criteria'][$id] = $criterion; $return['criteria'][$lastid] = $lastcriterion; } else { $return['criteria'][$id] = $criterion; } $lastaction = null; $lastid = $id; $this->nonjsbuttonpressed = true; } else if (array_key_exists('delete', $criterion)) { $this->nonjsbuttonpressed = true; } else { if (array_key_exists('movedown', $criterion)) { unset($criterion['movedown']); $lastaction = 'movedown'; $this->nonjsbuttonpressed = true; } $return['criteria'][$id] = $criterion; $lastid = $id; } } if ($totalscore <= 0) { $errors['err_totalscore'] = 1; } // add sort order field to criteria $csortorder = 1; foreach (array_keys($return['criteria']) as $id) { $return['criteria'][$id]['sortorder'] = $csortorder++; } // create validation error string (if needed) if ($withvalidation) { if (!$return['options']['lockzeropoints']) { if ($overallminscore == $overallmaxscore) { $errors['err_novariations'] = 1; } } if (count($errors)) { $rv = array(); foreach ($errors as $error => $v) { $rv[] = get_string($error, 'gradingform_rubric'); } $this->validationerrors = join('
', $rv); } else { $this->validationerrors = false; } $this->wasvalidated = true; } return $return; } /** * Scans array $ids to find the biggest element ! NEWID*, increments it by 1 and returns * * @param array $ids * @return string */ protected function get_next_id($ids) { $maxid = 0; foreach ($ids as $id) { if (preg_match('/^NEWID(\d+)$/', $id, $matches) && ((int)$matches[1]) > $maxid) { $maxid = (int)$matches[1]; } } return 'NEWID'.($maxid+1); } /** * Checks if a submit button was pressed which is supposed to be processed on client side by JS * but user seem to have disabled JS in the browser. * (buttons 'add criteria', 'add level', 'move up', 'move down', etc.) * In this case the form containing this element is prevented from being submitted * * @param array $value * @return boolean true if non-submit button was pressed and not processed by JS */ public function non_js_button_pressed($value) { if ($this->nonjsbuttonpressed === null) { $this->prepare_data($value); } return $this->nonjsbuttonpressed; } /** * Validates that rubric has at least one criterion, at least two levels within one criterion, * each level has a valid score, all levels have filled definitions and all criteria * have filled descriptions * * @param array $value * @return string|false error text or false if no errors found */ public function validate($value) { if (!$this->wasvalidated) { $this->prepare_data($value, true); } return $this->validationerrors; } /** * Prepares the data for saving * * @see prepare_data() * @param array $submitValues * @param boolean $assoc * @return array */ public function exportValue(&$submitValues, $assoc = false) { $value = $this->prepare_data($this->_findValue($submitValues)); return $this->_prepareValue($value, $assoc); } } PK'\$ 1form/rubric/amd/src/grades/grader/gradingpanel.jsnu[// 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 . /** * Grading panel for gradingform_rubric. * * @module gradingform_rubric/grades/grader/gradingpanel * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {call as fetchMany} from 'core/ajax'; import {normaliseResult} from 'core_grades/grades/grader/gradingpanel/normalise'; import {compareData} from 'core_grades/grades/grader/gradingpanel/comparison'; // Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send() import jQuery from 'jquery'; /** * For a given component, contextid, itemname & gradeduserid we can fetch the currently assigned grade. * * @param {String} component * @param {Number} contextid * @param {String} itemname * @param {Number} gradeduserid * * @returns {Promise} */ export const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => { return fetchMany([{ methodname: `gradingform_rubric_grader_gradingpanel_fetch`, args: { component, contextid, itemname, gradeduserid, }, }])[0]; }; /** * For a given component, contextid, itemname & gradeduserid we can store the currently assigned grade in a given form. * * @param {String} component * @param {Number} contextid * @param {String} itemname * @param {Number} gradeduserid * @param {Boolean} notifyUser * @param {HTMLElement} rootNode * * @returns {Promise} */ export const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, notifyUser, rootNode) => { const form = rootNode.querySelector('form'); if (compareData(form) === true) { return normaliseResult(await fetchMany([{ methodname: `gradingform_rubric_grader_gradingpanel_store`, args: { component, contextid, itemname, gradeduserid, notifyuser: notifyUser, formdata: jQuery(form).serialize(), }, }])[0]); } else { return ''; } }; PK'\=v7form/rubric/amd/build/grades/grader/gradingpanel.min.jsnu[define("gradingform_rubric/grades/grader/gradingpanel",["exports","core/ajax","core_grades/grades/grader/gradingpanel/normalise","core_grades/grades/grader/gradingpanel/comparison","jquery"],(function(_exports,_ajax,_normalise,_comparison,_jquery){var obj; /** * Grading panel for gradingform_rubric. * * @module gradingform_rubric/grades/grader/gradingpanel * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.storeCurrentGrade=_exports.fetchCurrentGrade=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.fetchCurrentGrade=(component,contextid,itemname,gradeduserid)=>(0,_ajax.call)([{methodname:"gradingform_rubric_grader_gradingpanel_fetch",args:{component:component,contextid:contextid,itemname:itemname,gradeduserid:gradeduserid}}])[0];_exports.storeCurrentGrade=async(component,contextid,itemname,gradeduserid,notifyUser,rootNode)=>{const form=rootNode.querySelector("form");return!0===(0,_comparison.compareData)(form)?(0,_normalise.normaliseResult)(await(0,_ajax.call)([{methodname:"gradingform_rubric_grader_gradingpanel_store",args:{component:component,contextid:contextid,itemname:itemname,gradeduserid:gradeduserid,notifyuser:notifyUser,formdata:(0,_jquery.default)(form).serialize()}}])[0]):""}})); //# sourceMappingURL=gradingpanel.min.js.mapPK'\! ;form/rubric/amd/build/grades/grader/gradingpanel.min.js.mapnu[{"version":3,"file":"gradingpanel.min.js","sources":["../../../src/grades/grader/gradingpanel.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Grading panel for gradingform_rubric.\n *\n * @module gradingform_rubric/grades/grader/gradingpanel\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport {normaliseResult} from 'core_grades/grades/grader/gradingpanel/normalise';\nimport {compareData} from 'core_grades/grades/grader/gradingpanel/comparison';\n\n// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()\nimport jQuery from 'jquery';\n\n/**\n * For a given component, contextid, itemname & gradeduserid we can fetch the currently assigned grade.\n *\n * @param {String} component\n * @param {Number} contextid\n * @param {String} itemname\n * @param {Number} gradeduserid\n *\n * @returns {Promise}\n */\nexport const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => {\n return fetchMany([{\n methodname: `gradingform_rubric_grader_gradingpanel_fetch`,\n args: {\n component,\n contextid,\n itemname,\n gradeduserid,\n },\n }])[0];\n};\n\n/**\n * For a given component, contextid, itemname & gradeduserid we can store the currently assigned grade in a given form.\n *\n * @param {String} component\n * @param {Number} contextid\n * @param {String} itemname\n * @param {Number} gradeduserid\n * @param {Boolean} notifyUser\n * @param {HTMLElement} rootNode\n *\n * @returns {Promise}\n */\nexport const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, notifyUser, rootNode) => {\n const form = rootNode.querySelector('form');\n\n if (compareData(form) === true) {\n return normaliseResult(await fetchMany([{\n methodname: `gradingform_rubric_grader_gradingpanel_store`,\n args: {\n component,\n contextid,\n itemname,\n gradeduserid,\n notifyuser: notifyUser,\n formdata: jQuery(form).serialize(),\n },\n }])[0]);\n } else {\n return '';\n }\n};\n"],"names":["component","contextid","itemname","gradeduserid","methodname","args","async","notifyUser","rootNode","form","querySelector","notifyuser","formdata","serialize"],"mappings":";;;;;;;6MAwCiC,CAACA,UAAWC,UAAWC,SAAUC,gBACvD,cAAU,CAAC,CACdC,0DACAC,KAAM,CACFL,UAAAA,UACAC,UAAAA,UACAC,SAAAA,SACAC,aAAAA,iBAEJ,8BAeyBG,MAAMN,UAAWC,UAAWC,SAAUC,aAAcI,WAAYC,kBACvFC,KAAOD,SAASE,cAAc,eAEV,KAAtB,2BAAYD,OACL,oCAAsB,cAAU,CAAC,CACpCL,0DACAC,KAAM,CACFL,UAAAA,UACAC,UAAAA,UACAC,SAAAA,SACAC,aAAAA,aACAQ,WAAYJ,WACZK,UAAU,mBAAOH,MAAMI,gBAE3B,IAEG"}PK'\(W\=\=Dform/rubric/tests/grades/grader/gradingpanel/external/fetch_test.phpnu[. declare(strict_types = 1); namespace gradingform_rubric\grades\grader\gradingpanel\external; use advanced_testcase; use coding_exception; use core_grades\component_gradeitem; use core_grades\component_gradeitems; use core_external\external_api; use mod_forum\local\entities\forum as forum_entity; use moodle_exception; /** * Unit tests for core_grades\component_gradeitems; * * @package gradingform_rubric * @category test * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class fetch_test extends advanced_testcase { /** * Ensure that an execute with an invalid component is rejected. */ public function test_execute_invalid_component(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(coding_exception::class); $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component"); fetch::execute('mod_invalid', 1, 'foo', 2); } /** * Ensure that an execute with an invalid itemname on a valid component is rejected. */ public function test_execute_invalid_itemname(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(coding_exception::class); $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component"); fetch::execute('mod_forum', 1, 'foo', 2); } /** * Ensure that an execute against a different grading method is rejected. */ public function test_execute_incorrect_type(): void { $this->resetAfterTest(); $forum = $this->get_forum_instance([ // Negative numbers mean a scale. 'grade_forum' => 5, ]); $course = $forum->get_course_record(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setUser($teacher); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $this->expectException(moodle_exception::class); $this->expectExceptionMessage("not configured for advanced grading with a rubric"); fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id); } /** * Ensure that an execute against the correct grading method returns the current state of the user. */ public function test_execute_fetch_empty(): void { $this->resetAfterTest(); [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, ] = $this->get_test_data(); $this->setUser($teacher); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id); $result = external_api::clean_returnvalue(fetch::execute_returns(), $result); $this->assertIsArray($result); $this->assertArrayHasKey('templatename', $result); $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']); $this->assertArrayHasKey('warnings', $result); $this->assertIsArray($result['warnings']); $this->assertEmpty($result['warnings']); // Test the grade array items. $this->assertArrayHasKey('grade', $result); $this->assertIsArray($result['grade']); $this->assertIsInt($result['grade']['timecreated']); $this->assertArrayHasKey('timemodified', $result['grade']); $this->assertIsInt($result['grade']['timemodified']); $this->assertArrayHasKey('usergrade', $result['grade']); $this->assertEquals('- / 100.00', $result['grade']['usergrade']); $this->assertArrayHasKey('maxgrade', $result['grade']); $this->assertIsInt($result['grade']['maxgrade']); $this->assertEquals(100, $result['grade']['maxgrade']); $this->assertArrayHasKey('gradedby', $result['grade']); $this->assertEquals(null, $result['grade']['gradedby']); $this->assertArrayHasKey('criteria', $result['grade']); $criteria = $result['grade']['criteria']; $this->assertCount(count($definition->rubric_criteria), $criteria); foreach ($criteria as $criterion) { $this->assertArrayHasKey('id', $criterion); $criterionid = $criterion['id']; $sourcecriterion = $definition->rubric_criteria[$criterionid]; $this->assertArrayHasKey('description', $criterion); $this->assertEquals($sourcecriterion['description'], $criterion['description']); $this->assertArrayHasKey('levels', $criterion); $levels = $criterion['levels']; foreach ($levels as $level) { $levelid = $level['id']; if (!isset($levelid)) { continue; } $sourcelevel = $sourcecriterion['levels'][$levelid]; $this->assertArrayHasKey('criterionid', $level); $this->assertEquals($criterionid, $level['criterionid']); $this->assertArrayHasKey('checked', $level); $this->assertArrayHasKey('definition', $level); $this->assertEquals($sourcelevel['definition'], $level['definition']); $this->assertArrayHasKey('score', $level); $this->assertEquals($sourcelevel['score'], $level['score']); } } } /** * Ensure that an execute against the correct grading method returns the current state of the user. */ public function test_execute_fetch_graded(): void { $this->resetAfterTest(); [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, ] = $this->get_test_data(); $this->execute_and_assert_fetch($forum, $controller, $definition, $teacher, $teacher, $student); } /** * Class mates should not get other's grades. */ public function test_execute_fetch_does_not_return_data_to_other_students(): void { $this->resetAfterTest(); [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, 'course' => $course, ] = $this->get_test_data(); $evilstudent = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->expectException(\required_capability_exception::class); $this->execute_and_assert_fetch($forum, $controller, $definition, $evilstudent, $teacher, $student); } /** * Grades can be returned to graded user. */ public function test_execute_fetch_return_data_to_graded_user(): void { $this->resetAfterTest(); [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, ] = $this->get_test_data(); $this->execute_and_assert_fetch($forum, $controller, $definition, $student, $teacher, $student); } /** * Executes and performs all the assertions of the fetch method with the given parameters. */ private function execute_and_assert_fetch($forum, $controller, $definition, $fetcheruser, $grader, $gradeduser) { $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); $this->setUser($grader); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $grade = $gradeitem->get_grade_for_user($gradeduser, $grader); $instance = $gradeitem->get_advanced_grading_instance($grader, $grade); $submissiondata = $rubricgenerator->get_test_form_data($controller, (int) $gradeduser->id, 0, 'Too many mistakes. Please try again.', 2, 'Great number of pictures. Well done.' ); $gradeitem->store_grade_from_formdata($gradeduser, $grader, (object) [ 'instanceid' => $instance->get_id(), 'advancedgrading' => $submissiondata, ]); $this->setUser($fetcheruser); $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $gradeduser->id); $result = external_api::clean_returnvalue(fetch::execute_returns(), $result); $this->assertIsArray($result); $this->assertArrayHasKey('templatename', $result); $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']); $this->assertArrayHasKey('warnings', $result); $this->assertIsArray($result['warnings']); $this->assertEmpty($result['warnings']); // Test the grade array items. $this->assertArrayHasKey('grade', $result); $this->assertIsArray($result['grade']); $this->assertIsInt($result['grade']['timecreated']); $this->assertArrayHasKey('timemodified', $result['grade']); $this->assertIsInt($result['grade']['timemodified']); $this->assertArrayHasKey('usergrade', $result['grade']); $this->assertEquals('50.00 / 100.00', $result['grade']['usergrade']); $this->assertArrayHasKey('maxgrade', $result['grade']); $this->assertIsInt($result['grade']['maxgrade']); $this->assertEquals(100, $result['grade']['maxgrade']); $this->assertArrayHasKey('gradedby', $result['grade']); $this->assertEquals(fullname($grader), $result['grade']['gradedby']); $this->assertArrayHasKey('criteria', $result['grade']); $criteria = $result['grade']['criteria']; $this->assertCount(count($definition->rubric_criteria), $criteria); foreach ($criteria as $criterion) { $this->assertArrayHasKey('id', $criterion); $criterionid = $criterion['id']; $sourcecriterion = $definition->rubric_criteria[$criterionid]; $this->assertArrayHasKey('description', $criterion); $this->assertEquals($sourcecriterion['description'], $criterion['description']); $this->assertArrayHasKey('remark', $criterion); $this->assertArrayHasKey('levels', $criterion); $levels = $criterion['levels']; foreach ($levels as $level) { $levelid = $level['id']; if (!isset($levelid)) { continue; } $sourcelevel = $sourcecriterion['levels'][$levelid]; $this->assertArrayHasKey('criterionid', $level); $this->assertEquals($criterionid, $level['criterionid']); $this->assertArrayHasKey('checked', $level); $this->assertArrayHasKey('definition', $level); $this->assertEquals($sourcelevel['definition'], $level['definition']); $this->assertArrayHasKey('score', $level); $this->assertEquals($sourcelevel['score'], $level['score']); } } $this->assertEquals(1, $criteria[0]['levels'][1]['checked']); $this->assertEquals('Too many mistakes. Please try again.', $criteria[0]['remark']); $this->assertEquals(1, $criteria[1]['levels'][3]['checked']); $this->assertEquals('Great number of pictures. Well done.', $criteria[1]['remark']); } /** * Get a forum instance. * * @param array $config * @return forum_entity */ protected function get_forum_instance(array $config = []): forum_entity { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id, 'grade_forum' => 100])); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_forum_vault(); return $vault->get_from_id((int) $forum->id); } /** * Get test data for forums graded using a rubric. * * @return array */ protected function get_test_data(): array { global $DB; $this->resetAfterTest(); $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); $forum = $this->get_forum_instance(); $course = $forum->get_course_record(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setUser($teacher); $controller = $rubricgenerator->get_test_rubric($forum->get_context(), 'forum', 'forum'); $definition = $controller->get_definition(); // In the situation of mod_forum this would be the id from forum_grades. $itemid = 1; $instance = $controller->create_instance($student->id, $itemid); $data = $this->get_test_form_data( $controller, $itemid, 1, 'This user made several mistakes.', 0, 'Please add more pictures.' ); // Update this instance with data. $instance->update($data); return [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, 'course' => $course, ]; } /** * Fetch a set of sample data. * * @param \gradingform_rubric_controller $controller * @param int $itemid * @param float $spellingscore * @param string $spellingremark * @param float $picturescore * @param string $pictureremark * @return array */ protected function get_test_form_data( \gradingform_rubric_controller $controller, int $itemid, float $spellingscore, string $spellingremark, float $picturescore, string $pictureremark ): array { $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); return $rubricgenerator->get_test_form_data( $controller, $itemid, $spellingscore, $spellingremark, $picturescore, $pictureremark ); } } PK'\x x'x'Dform/rubric/tests/grades/grader/gradingpanel/external/store_test.phpnu[. declare(strict_types = 1); namespace gradingform_rubric\grades\grader\gradingpanel\external; use advanced_testcase; use coding_exception; use core_grades\component_gradeitem; use core_external\external_api; use mod_forum\local\entities\forum as forum_entity; use moodle_exception; /** * Unit tests for core_grades\component_gradeitems; * * @package gradingform_rubric * @category test * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class store_test extends advanced_testcase { /** * Ensure that an execute with an invalid component is rejected. */ public function test_execute_invalid_component(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(coding_exception::class); $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component"); store::execute('mod_invalid', 1, 'foo', 2, false, 'formdata'); } /** * Ensure that an execute with an invalid itemname on a valid component is rejected. */ public function test_execute_invalid_itemname(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(coding_exception::class); $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component"); store::execute('mod_forum', 1, 'foo', 2, false, 'formdata'); } /** * Ensure that an execute against a different grading method is rejected. */ public function test_execute_incorrect_type(): void { $this->resetAfterTest(); $forum = $this->get_forum_instance([ 'grade_forum' => 5, ]); $course = $forum->get_course_record(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setUser($teacher); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $this->expectException(moodle_exception::class); store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, false, 'formdata'); } /** * Ensure that an execute against a different grading method is rejected. */ public function test_execute_disabled(): void { $this->resetAfterTest(); $forum = $this->get_forum_instance(); $course = $forum->get_course_record(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setUser($teacher); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $this->expectException(moodle_exception::class); $this->expectExceptionMessage("Grading is not enabled"); store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, false, 'formdata'); } /** * Ensure that an execute against the correct grading method returns the current state of the user. */ public function test_execute_store_graded(): void { $this->resetAfterTest(); $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, ] = $this->get_test_data(); $this->setUser($teacher); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $grade = $gradeitem->get_grade_for_user($student, $teacher); $instance = $gradeitem->get_advanced_grading_instance($teacher, $grade); $submissiondata = $rubricgenerator->get_test_form_data($controller, (int) $student->id, 0, 'Too many mistakes. Please try again.', 2, 'Great number of pictures. Well done.' ); $formdata = http_build_query((object) [ 'instanceid' => $instance->get_id(), 'advancedgrading' => $submissiondata, ], '', '&'); $result = store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, false, $formdata); $result = external_api::clean_returnvalue(store::execute_returns(), $result); $this->assertIsArray($result); $this->assertArrayHasKey('templatename', $result); $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']); $this->assertArrayHasKey('warnings', $result); $this->assertIsArray($result['warnings']); $this->assertEmpty($result['warnings']); // Test the grade array items. $this->assertArrayHasKey('grade', $result); $this->assertIsArray($result['grade']); $this->assertIsInt($result['grade']['timecreated']); $this->assertArrayHasKey('timemodified', $result['grade']); $this->assertIsInt($result['grade']['timemodified']); $this->assertArrayHasKey('usergrade', $result['grade']); $this->assertEquals('1.00 / 2.00', $result['grade']['usergrade']); $this->assertArrayHasKey('maxgrade', $result['grade']); $this->assertIsInt($result['grade']['maxgrade']); $this->assertEquals(2, $result['grade']['maxgrade']); $this->assertArrayHasKey('gradedby', $result['grade']); $this->assertEquals(fullname($teacher), $result['grade']['gradedby']); $this->assertArrayHasKey('criteria', $result['grade']); $criteria = $result['grade']['criteria']; $this->assertCount(count($definition->rubric_criteria), $criteria); foreach ($criteria as $criterion) { $this->assertArrayHasKey('id', $criterion); $criterionid = $criterion['id']; $sourcecriterion = $definition->rubric_criteria[$criterionid]; $this->assertArrayHasKey('description', $criterion); $this->assertEquals($sourcecriterion['description'], $criterion['description']); $this->assertArrayHasKey('remark', $criterion); $this->assertArrayHasKey('levels', $criterion); $levels = $criterion['levels']; foreach ($levels as $level) { $levelid = $level['id']; if (!isset($levelid)) { continue; } $sourcelevel = $sourcecriterion['levels'][$levelid]; $this->assertArrayHasKey('criterionid', $level); $this->assertEquals($criterionid, $level['criterionid']); $this->assertArrayHasKey('checked', $level); $this->assertArrayHasKey('definition', $level); $this->assertEquals($sourcelevel['definition'], $level['definition']); $this->assertArrayHasKey('score', $level); $this->assertEquals($sourcelevel['score'], $level['score']); } } $this->assertEquals(1, $criteria[0]['levels'][1]['checked']); $this->assertEquals('Too many mistakes. Please try again.', $criteria[0]['remark']); $this->assertEquals(1, $criteria[1]['levels'][3]['checked']); $this->assertEquals('Great number of pictures. Well done.', $criteria[1]['remark']); } /** * Get a forum instance. * * @param array $config * @return forum_entity */ protected function get_forum_instance(array $config = []): forum_entity { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id])); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_forum_vault(); return $vault->get_from_id((int) $forum->id); } /** * Get test data for forums graded using a rubric. * * @return array */ protected function get_test_data(): array { global $DB; $this->resetAfterTest(); $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); $forum = $this->get_forum_instance(); $course = $forum->get_course_record(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setUser($teacher); $controller = $rubricgenerator->get_test_rubric($forum->get_context(), 'forum', 'forum'); $definition = $controller->get_definition(); $DB->set_field('forum', 'grade_forum', count($definition->rubric_criteria), ['id' => $forum->get_id()]); return [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, ]; } } PK'\!&form/rubric/tests/generator/rubric.phpnu[. /** * Generator for the gradingforum_rubric plugin. * * @package gradingform_rubric * @category test * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tests\gradingform_rubric\generator; use gradingform_controller; use stdClass; /** * Test rubric. * * @package gradingform_rubric * @category test * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class rubric { /** @var array $criteria The criteria for this rubric. */ protected $criteria = []; /** @var string The name of this rubric. */ protected $name; /** @var string A description for this rubric. */ protected $description; /** @var array The rubric options. */ protected $options = []; /** * Create a new gradingform_rubric_generator_rubric. * * @param string $name * @param string $description */ public function __construct(string $name, string $description) { $this->name = $name; $this->description = $description; $this->set_option('sortlevelsasc', 1); $this->set_option('lockzeropoints', 1); $this->set_option('showdescriptionteacher', 1); $this->set_option('showdescriptionstudent', 1); $this->set_option('showscoreteacher', 1); $this->set_option('showscorestudent', 1); $this->set_option('enableremarks', 1); $this->set_option('showremarksstudent', 1); } /** * Creates the rubric using the appropriate APIs. */ public function get_definition(): stdClass { return (object) [ 'name' => $this->name, 'description_editor' => [ 'text' => $this->description, 'format' => FORMAT_HTML, 'itemid' => 1 ], 'rubric' => [ 'criteria' => $this->get_all_criterion_values(), 'options' => $this->options, ], 'saverubric' => 'Save rubric and make it ready', 'status' => gradingform_controller::DEFINITION_STATUS_READY, ]; } /** * Set an option for the rubric. * * @param string $key * @param mixed $value * @return self */ public function set_option(string $key, $value): self { $this->options[$key] = $value; return $this; } /** * Adds a criterion to the rubric. * * @param criterion $criterion The criterion object (class below). * @return self */ public function add_criteria(criterion $criterion): self { $this->criteria[] = $criterion; return $this; } /** * Get all criterion values. * * @return array */ protected function get_all_criterion_values(): array { $result = []; foreach ($this->criteria as $index => $criterion) { $id = $index + 1; $result["NEWID{$id}"] = $criterion->get_all_values($id); } return $result; } } PK'\򭥴Z Z #form/rubric/tests/generator/lib.phpnu[. /** * Generator for the gradingforum_rubric plugin. * * @package gradingform_rubric * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/rubric.php'); require_once(__DIR__ . '/criterion.php'); use tests\gradingform_rubric\generator\rubric; use tests\gradingform_rubric\generator\criterion; /** * Generator for the gradingforum_rubric plugintype. * * @package gradingform_rubric * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradingform_rubric_generator extends component_generator_base { /** * Create an instance of a rubric. * * @param context $context * @param string $component * @param string $area * @param string $name * @param string $description * @param array $criteria The list of criteria to add to the generated rubric * @return gradingform_rubric_controller */ public function create_instance( context $context, string $component, string $area, string $name, string $description, array $criteria ): gradingform_rubric_controller { global $USER; if ($USER->id === 0) { throw new \coding_exception('Creation of a rubric must currently be run as a user.'); } // Fetch the controller for this context/component/area. $generator = \testing_util::get_data_generator(); $gradinggenerator = $generator->get_plugin_generator('core_grading'); $controller = $gradinggenerator->create_instance($context, $component, $area, 'rubric'); // Generate a definition for the supplied rubric. $rubric = $this->get_rubric($name, $description); foreach ($criteria as $name => $criterion) { $rubric->add_criteria($this->get_criterion($name, $criterion)); } // Update the controller wih the rubric definition. $controller->update_definition($rubric->get_definition()); return $controller; } /** * Get a new rubric for use with the rubric controller. * * Note: This is just a helper class used to build a new definition. It does not persist the data. * * @param string $name * @param string $description * @return rubric */ protected function get_rubric(string $name, string $description): rubric { return new rubric($name, $description); } /** * Get a new rubric for use with a gradingform_rubric_generator_rubric. * * Note: This is just a helper class used to build a new definition. It does not persist the data. * * @param string $description * @param array $levels Set of levels in the form definition => score * @return gradingform_rubric_generator_criterion */ protected function get_criterion(string $description, array $levels = []): criterion { return new criterion($description, $levels); } /** * Given a controller instance, fetch the level and criterion information for the specified values. * * @param gradingform_controller $controller * @param string $description The description to match the criterion on * @param float $score The value to match the level on * @return array */ public function get_level_and_criterion_for_values( gradingform_controller $controller, string $description, float $score ): array { $definition = $controller->get_definition(); $criteria = $definition->rubric_criteria; $criterion = $level = null; $criterion = array_reduce($criteria, function($carry, $criterion) use ($description) { if ($criterion['description'] === $description) { $carry = $criterion; } return $carry; }, null); if ($criterion) { $criterion = (object) $criterion; $level = array_reduce($criterion->levels, function($carry, $level) use ($score) { if ($level['score'] == $score) { $carry = $level; } return $carry; }); $level = $level ? (object) $level : null; } return [ 'criterion' => $criterion, 'level' => $level, ]; } /** * Get submitted form data for the supplied controller, itemid, and values. * The returned data is in the format used by rubric when handling form submission. * * @param gradingform_rubric_controller $controller * @param int $itemid * @param array $values A set of array values where the array key is the name of the criterion, and the value is an * array with the desired score, and any remark. */ public function get_submitted_form_data(gradingform_rubric_controller $controller, int $itemid, array $values): array { $result = [ 'itemid' => $itemid, 'criteria' => [], ]; foreach ($values as $criterionname => ['score' => $score, 'remark' => $remark]) { [ 'criterion' => $criterion, 'level' => $level, ] = $this->get_level_and_criterion_for_values($controller, $criterionname, $score); $result['criteria'][$criterion->id] = [ 'levelid' => $level->id, 'remark' => $remark, ]; } return $result; } /** * Generate a rubric controller with sample data required for testing of this class. * * @param context $context * @param string $component * @param string $area * @return gradingform_rubric_controller */ public function get_test_rubric(context $context, string $component, string $area): gradingform_rubric_controller { $criteria = [ 'Spelling is important' => [ 'Nothing but mistakes' => 0, 'Several mistakes' => 1, 'No mistakes' => 2, ], 'Pictures' => [ 'No pictures' => 0, 'One picture' => 1, 'More than one picture' => 2, ], ]; return $this->create_instance($context, $component, $area, 'testrubric', 'Description text', $criteria); } /** * Fetch a set of sample data. * * @param gradingform_rubric_controller $controller * @param int $itemid * @param float $spellingscore * @param string $spellingremark * @param float $picturescore * @param string $pictureremark * @return array */ public function get_test_form_data( gradingform_rubric_controller $controller, int $itemid, float $spellingscore, string $spellingremark, float $picturescore, string $pictureremark ): array { $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); return $rubricgenerator->get_submitted_form_data($controller, $itemid, [ 'Spelling is important' => [ 'score' => $spellingscore, 'remark' => $spellingremark, ], 'Pictures' => [ 'score' => $picturescore, 'remark' => $pictureremark, ], ]); } } PK'\1 1 )form/rubric/tests/generator/criterion.phpnu[. /** * Generator for the gradingforum_rubric plugin. * * @package gradingform_rubric * @category test * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tests\gradingform_rubric\generator; /** * Convenience class to create rubric criterion. * * @package gradingform_rubric * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class criterion { /** @var string $description A description of the criterion. */ public $description; /** @var integer $sortorder sort order of the criterion. */ public $sortorder = 0; /** @var array $levels The levels for this criterion. */ public $levels = []; /** * Constructor for this test_criterion object * * @param string $description A description of this criterion. * @param array $levels */ public function __construct(string $description, array $levels = []) { $this->description = $description; foreach ($levels as $definition => $score) { $this->add_level($definition, $score); } } /** * Adds levels to the criterion. * * @param string $definition The definition for this level. * @param int $score The score received if this level is selected. * @return self */ public function add_level(string $definition, int $score): self { $this->levels[] = [ 'definition' => $definition, 'score' => $score ]; return $this; } /** * Get the description for this criterion. * * @return string */ public function get_description(): string { return $this->description; } /** * Get the levels for this criterion. * * @return array */ public function get_levels(): array { return $this->levels; } /** * Get all values in an array for use when creating a new guide. * * @param int $sortorder * @return array */ public function get_all_values(int $sortorder): array { return [ 'sortorder' => $sortorder, 'description' => $this->get_description(), 'levels' => $this->get_all_level_values(), ]; } /** * Get all level values. * * @return array */ public function get_all_level_values(): array { $result = []; foreach ($this->get_levels() as $index => $level) { $id = $index + 1; $result["NEWID{$id}"] = $level; } return $result; } } PK'\"+form/rubric/tests/privacy/provider_test.phpnu[. /** * Privacy tests for gradingform_rubric * * @package gradingform_rubric * @category test * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace gradingform_rubric\privacy; use core_privacy\tests\provider_testcase; use core_privacy\local\request\writer; use gradingform_rubric\privacy\provider; use gradingform_rubric_controller; use context_module; /** * Privacy tests for gradingform_rubric * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { /** * Test the export of rubric data. */ public function test_get_gradingform_export_data(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $module = $this->getDataGenerator()->create_module('assign', ['course' => $course]); $modulecontext = context_module::instance($module->cmid); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Generate a test rubric and get its controller. $controller = $this->get_test_rubric($modulecontext, 'assign', 'submissions'); // In the situation of mod_assign this would be the id from assign_grades. $itemid = 1; $instance = $controller->create_instance($user->id, $itemid); $data = $this->get_test_form_data( $controller, $itemid, 1, 'This user made several mistakes.', 0, 'Please add more pictures.' ); // Update this instance with data. $instance->update($data); $instanceid = $instance->get_data('id'); // Let's try the method we are testing. provider::export_gradingform_instance_data($modulecontext, $instance->get_id(), ['Test']); $data = (array) writer::with_context($modulecontext)->get_data(['Test', 'Rubric', $instanceid]); $this->assertCount(2, $data); $this->assertEquals('Spelling is important', $data['Spelling is important']->description); $this->assertEquals('This user made several mistakes.', $data['Spelling is important']->remark); $this->assertEquals('Pictures', $data['Pictures']->description); $this->assertEquals('Please add more pictures.', $data['Pictures']->remark); } /** * Test the deletion of rubric user information via the instance ID. */ public function test_delete_gradingform_for_instances(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $module = $this->getDataGenerator()->create_module('assign', ['course' => $course]); $modulecontext = context_module::instance($module->cmid); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Generate a test rubric and get its controller. $controller = $this->get_test_rubric($modulecontext, 'assign', 'submissions'); // In the situation of mod_assign this would be the id from assign_grades. $itemid = 1; $instance = $controller->create_instance($user->id, $itemid); $data = $this->get_test_form_data( $controller, $itemid, 1, 'This user made several mistakes.', 0, 'Please add more pictures.' ); // Update this instance with data. $instance->update($data); // Second instance. $itemid = 2; $instance = $controller->create_instance($user->id, $itemid); $data = $this->get_test_form_data( $controller, $itemid, 0, 'Too many mistakes. Please try again.', 2, 'Great number of pictures. Well done.' ); // Update this instance with data. $instance->update($data); // Check how many records we have in the fillings table. $records = $DB->get_records('gradingform_rubric_fillings'); $this->assertCount(4, $records); // Let's delete one of the instances (the last one would be the easiest). provider::delete_gradingform_for_instances([$instance->get_id()]); $records = $DB->get_records('gradingform_rubric_fillings'); $this->assertCount(2, $records); foreach ($records as $record) { $this->assertNotEquals($instance->get_id(), $record->instanceid); } } /** * Generate a rubric controller with sample data required for testing of this class. * * @param context_module $context * @param string $component * @param string $area * @return gradingform_rubric_controller */ protected function get_test_rubric(context_module $context, string $component, string $area): gradingform_rubric_controller { $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); return $rubricgenerator->get_test_rubric($context, $component, $area); } /** * Fetch a set of sample data. * * @param gradingform_rubric_controller $controller * @param int $itemid * @param float $spellingscore * @param string $spellingremark * @param float $picturescore * @param string $pictureremark * @return array */ protected function get_test_form_data( gradingform_rubric_controller $controller, int $itemid, float $spellingscore, string $spellingremark, float $picturescore, string $pictureremark ): array { $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); return $rubricgenerator->get_test_form_data( $controller, $itemid, $spellingscore, $spellingremark, $picturescore, $pictureremark ); } } PK'\5s|33$form/rubric/tests/generator_test.phpnu[. /** * Generator testcase for the gradingforum_rubric generator. * * @package gradingform_rubric * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace gradingform_rubric; use advanced_testcase; use context_module; use gradingform_rubric_controller; use gradingform_controller; /** * Generator testcase for the gradingforum_rubric generator. * * @package gradingform_rubric * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class generator_test extends advanced_testcase { /** * Test rubric creation. */ public function test_rubric_creation(): void { $this->resetAfterTest(true); // Fetch generators. $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); // Create items required for testing. $course = $generator->create_course(); $module = $generator->create_module('assign', ['course' => $course]); $user = $generator->create_user(); $context = context_module::instance($module->cmid); // Data for testing. $name = 'myfirstrubric'; $description = 'My first rubric'; $criteria = [ 'Alphabet' => [ 'Not known' => 0, 'Letters known but out of order' => 1, 'Letters known in order ascending' => 2, 'Letters known and can recite forwards and backwards' => 4, ], 'Times tables' => [ 'Not known' => 0, '2 times table known' => 2, '2 and 5 times table known' => 4, '2, 5, and 10 times table known' => 8, ], ]; // Unit under test. $this->setUser($user); $controller = $rubricgenerator->create_instance($context, 'mod_assign', 'submission', $name, $description, $criteria); $this->assertInstanceOf(gradingform_rubric_controller::class, $controller); $definition = $controller->get_definition(); $this->assertNotEmpty($definition->id); $this->assertEquals($name, $definition->name); $this->assertEquals($description, $definition->description); $this->assertEquals(gradingform_controller::DEFINITION_STATUS_READY, $definition->status); $this->assertNotEmpty($definition->timecreated); $this->assertNotEmpty($definition->timemodified); $this->assertEquals($user->id, $definition->usercreated); $this->assertNotEmpty($definition->rubric_criteria); $this->assertCount(2, $definition->rubric_criteria); // Check the alphabet criteria. $criteriaids = array_keys($definition->rubric_criteria); $alphabet = $definition->rubric_criteria[$criteriaids[0]]; $this->assertNotEmpty($alphabet['id']); $this->assertEquals(1, $alphabet['sortorder']); $this->assertEquals('Alphabet', $alphabet['description']); $this->assertNotEmpty($alphabet['levels']); $levels = $alphabet['levels']; $levelids = array_keys($levels); $level = $levels[$levelids[0]]; $this->assertEquals(0, $level['score']); $this->assertEquals('Not known', $level['definition']); $level = $levels[$levelids[1]]; $this->assertEquals(1, $level['score']); $this->assertEquals('Letters known but out of order', $level['definition']); $level = $levels[$levelids[2]]; $this->assertEquals(2, $level['score']); $this->assertEquals('Letters known in order ascending', $level['definition']); $level = $levels[$levelids[3]]; $this->assertEquals(4, $level['score']); $this->assertEquals('Letters known and can recite forwards and backwards', $level['definition']); // Check the times tables criteria. $tables = $definition->rubric_criteria[$criteriaids[1]]; $this->assertNotEmpty($tables['id']); $this->assertEquals(2, $tables['sortorder']); $this->assertEquals('Times tables', $tables['description']); $this->assertNotEmpty($tables['levels']); $levels = $tables['levels']; $levelids = array_keys($levels); $level = $levels[$levelids[0]]; $this->assertEquals(0, $level['score']); $this->assertEquals('Not known', $level['definition']); $level = $levels[$levelids[1]]; $this->assertEquals(2, $level['score']); $this->assertEquals('2 times table known', $level['definition']); $level = $levels[$levelids[2]]; $this->assertEquals(4, $level['score']); $this->assertEquals('2 and 5 times table known', $level['definition']); $level = $levels[$levelids[3]]; $this->assertEquals(8, $level['score']); $this->assertEquals('2, 5, and 10 times table known', $level['definition']); } /** * Test the get_level_and_criterion_for_values function. * This is used for finding criterion and level information within a rubric. */ public function test_get_level_and_criterion_for_values(): void { $this->resetAfterTest(true); // Fetch generators. $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); // Create items required for testing. $course = $generator->create_course(); $module = $generator->create_module('assign', ['course' => $course]); $user = $generator->create_user(); $context = context_module::instance($module->cmid); // Data for testing. $description = 'My first rubric'; $criteria = [ 'Alphabet' => [ 'Not known' => 0, 'Letters known but out of order' => 1, 'Letters known in order ascending' => 2, 'Letters known and can recite forwards and backwards' => 4, ], 'Times tables' => [ 'Not known' => 0, '2 times table known' => 2, '2 and 5 times table known' => 4, '2, 5, and 10 times table known' => 8, ], ]; $this->setUser($user); $controller = $rubricgenerator->create_instance($context, 'mod_assign', 'submission', 'rubric', $description, $criteria); // Valid criterion and level. $result = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Alphabet', 2); $this->assertEquals('Alphabet', $result['criterion']->description); $this->assertEquals('2', $result['level']->score); $this->assertEquals('Letters known in order ascending', $result['level']->definition); // Valid criterion. Invalid level. $result = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Alphabet', 3); $this->assertEquals('Alphabet', $result['criterion']->description); $this->assertNull($result['level']); // Invalid criterion. $result = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Foo', 0); $this->assertNull($result['criterion']); } /** * Tests for the get_test_rubric function. */ public function test_get_test_rubric(): void { $this->resetAfterTest(true); // Fetch generators. $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); // Create items required for testing. $course = $generator->create_course(); $module = $generator->create_module('assign', ['course' => $course]); $user = $generator->create_user(); $context = context_module::instance($module->cmid); $this->setUser($user); $rubric = $rubricgenerator->get_test_rubric($context, 'assign', 'submissions'); $definition = $rubric->get_definition(); $this->assertEquals('testrubric', $definition->name); $this->assertEquals('Description text', $definition->description); $this->assertEquals(gradingform_controller::DEFINITION_STATUS_READY, $definition->status); // Should create a rubric with 2 criterion. $this->assertCount(2, $definition->rubric_criteria); } /** * Test the get_submitted_form_data function. */ public function test_get_submitted_form_data(): void { $this->resetAfterTest(true); // Fetch generators. $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); // Create items required for testing. $course = $generator->create_course(); $module = $generator->create_module('assign', ['course' => $course]); $user = $generator->create_user(); $context = context_module::instance($module->cmid); $this->setUser($user); $controller = $rubricgenerator->get_test_rubric($context, 'assign', 'submissions'); $result = $rubricgenerator->get_submitted_form_data($controller, 93, [ 'Spelling is important' => [ 'score' => 1, 'remark' => 'Good speeling', ], 'Pictures' => [ 'score' => 2, 'remark' => 'Lots of nice pictures!', ] ]); $this->assertIsArray($result); $this->assertEquals(93, $result['itemid']); $this->assertIsArray($result['criteria']); $this->assertCount(2, $result['criteria']); $spelling = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Spelling is important', 1); $this->assertIsArray($result['criteria'][$spelling['criterion']->id]); $this->assertEquals($spelling['level']->id, $result['criteria'][$spelling['criterion']->id]['levelid']); $this->assertEquals('Good speeling', $result['criteria'][$spelling['criterion']->id]['remark']); $pictures = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Pictures', 2); $this->assertIsArray($result['criteria'][$pictures['criterion']->id]); $this->assertEquals($pictures['level']->id, $result['criteria'][$pictures['criterion']->id]['levelid']); $this->assertEquals('Lots of nice pictures!', $result['criteria'][$pictures['criterion']->id]['remark']); } /** * Test the get_test_form_data function. */ public function test_get_test_form_data(): void { $this->resetAfterTest(true); // Fetch generators. $generator = \testing_util::get_data_generator(); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); // Create items required for testing. $course = $generator->create_course(); $module = $generator->create_module('assign', ['course' => $course]); $user = $generator->create_user(); $context = context_module::instance($module->cmid); $this->setUser($user); $controller = $rubricgenerator->get_test_rubric($context, 'assign', 'submissions'); // Unit under test. $result = $rubricgenerator->get_test_form_data( $controller, 1839, 1, 'Propper good speling', 0, 'ASCII art is not a picture' ); $this->assertIsArray($result); $this->assertEquals(1839, $result['itemid']); $this->assertIsArray($result['criteria']); $this->assertCount(2, $result['criteria']); $spelling = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Spelling is important', 1); $this->assertIsArray($result['criteria'][$spelling['criterion']->id]); $this->assertEquals($spelling['level']->id, $result['criteria'][$spelling['criterion']->id]['levelid']); $this->assertEquals('Propper good speling', $result['criteria'][$spelling['criterion']->id]['remark']); $pictures = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Pictures', 0); $this->assertIsArray($result['criteria'][$pictures['criterion']->id]); $this->assertEquals($pictures['level']->id, $result['criteria'][$pictures['criterion']->id]['levelid']); $this->assertEquals('ASCII art is not a picture', $result['criteria'][$pictures['criterion']->id]['remark']); } } PK'\6jUU4form/rubric/tests/behat/behat_gradingform_rubric.phpnu[. /** * Steps definitions for rubrics. * * @package gradingform_rubric * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php'); use Behat\Gherkin\Node\TableNode; use Behat\Mink\Exception\ElementNotFoundException; use Behat\Mink\Exception\ExpectationException; /** * Steps definitions to help with rubrics. * * @package gradingform_rubric * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_gradingform_rubric extends behat_base { /** * @var The number of levels added by default when a rubric is created. */ const DEFAULT_RUBRIC_LEVELS = 3; /** * Defines the rubric with the provided data, following rubric's definition grid cells. * * This method fills the rubric of the rubric definition * form; the provided TableNode should contain one row for * each criterion and each cell of the row should contain: * # Criterion description * # Criterion level 1 name * # Criterion level 1 points * # Criterion level 2 name * # Criterion level 2 points * # Criterion level 3 ..... * * Works with both JS and non-JS. * * @When /^I define the following rubric:$/ * @throws ExpectationException * @param TableNode $rubric */ public function i_define_the_following_rubric(TableNode $rubric) { // Being a smart method is nothing good when we talk about step definitions, in // this case we didn't have any other options as there are no labels no elements // id we can point to without having to "calculate" them. $steptableinfo = '| criterion description | level1 name | level1 points | level2 name | level2 points | ...'; $criteria = $rubric->getRows(); $addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_rubric')); // Cleaning the current ones. $deletebuttons = $this->find_all('css', "input[value='" . get_string('criteriondelete', 'gradingform_rubric') . "']"); if ($deletebuttons) { // We should reverse the deletebuttons because otherwise once we delete // the first one the DOM will change and the [X] one will not exist anymore. $deletebuttons = array_reverse($deletebuttons, true); foreach ($deletebuttons as $button) { $this->click_and_confirm($button); } } // The level number (NEWID$N) is not reset after each criterion. $levelnumber = 1; // The next criterion is created with the same number of levels than the last criterion. $defaultnumberoflevels = self::DEFAULT_RUBRIC_LEVELS; if ($criteria) { foreach ($criteria as $criterionit => $criterion) { // Unset empty levels in criterion. foreach ($criterion as $i => $value) { if (empty($value)) { unset($criterion[$i]); } } // Remove empty criterion, as TableNode might contain them to make table rows equal size. $newcriterion = array(); foreach ($criterion as $k => $c) { if (!empty($c)) { $newcriterion[$k] = $c; } } $criterion = $newcriterion; // Checking the number of cells. if (count($criterion) % 2 === 0) { throw new ExpectationException( 'The criterion levels should contain both definition and points, follow this format:' . $steptableinfo, $this->getSession() ); } // Minimum 2 levels per criterion. // description + definition1 + score1 + definition2 + score2 = 5. if (count($criterion) < 5) { throw new ExpectationException( get_string('err_mintwolevels', 'gradingform_rubric'), $this->getSession() ); } // Add new criterion. $this->execute('behat_general::i_click_on', [ $addcriterionbutton, 'NodeElement', ]); $criterionroot = 'rubric[criteria][NEWID' . ($criterionit + 1) . ']'; // Getting the criterion description, this one is visible by default. $this->set_rubric_field_value($criterionroot . '[description]', array_shift($criterion), true); // When JS is disabled each criterion's levels name numbers starts from 0. if (!$this->running_javascript()) { $levelnumber = 0; } // Setting the correct number of levels. $nlevels = count($criterion) / 2; if ($nlevels < $defaultnumberoflevels) { // Removing levels if there are too much levels. // When we add a new level the NEWID$N is increased from the last criterion. $lastcriteriondefaultlevel = $defaultnumberoflevels + $levelnumber - 1; $lastcriterionlevel = $nlevels + $levelnumber - 1; for ($i = $lastcriteriondefaultlevel; $i > $lastcriterionlevel; $i--) { // If JS is disabled seems that new levels are not added. if ($this->running_javascript()) { $deletelevel = $this->find_button($criterionroot . '[levels][NEWID' . $i . '][delete]'); $this->click_and_confirm($deletelevel); } else { // Only if the level exists. $buttonname = $criterionroot . '[levels][NEWID' . $i . '][delete]'; if ($deletelevel = $this->getSession()->getPage()->findButton($buttonname)) { $this->execute('behat_general::i_click_on', [ $deletelevel, 'NodeElement', ]); } } } } else if ($nlevels > $defaultnumberoflevels) { // Adding levels if we don't have enough. $addlevel = $this->find_button($criterionroot . '[levels][addlevel]'); for ($i = ($defaultnumberoflevels + 1); $i <= $nlevels; $i++) { $this->execute('behat_general::i_click_on', [ $addlevel, 'NodeElement', ]); } } // Updating it. if ($nlevels > self::DEFAULT_RUBRIC_LEVELS) { $defaultnumberoflevels = $nlevels; } else { // If it is less than the default value it sets it to // the default value. $defaultnumberoflevels = self::DEFAULT_RUBRIC_LEVELS; } foreach ($criterion as $i => $value) { $levelroot = $criterionroot . '[levels][NEWID' . $levelnumber . ']'; if ($i % 2 === 0) { // Pairs are the definitions. $fieldname = $levelroot . '[definition]'; $this->set_rubric_field_value($fieldname, $value); } else { // Odds are the points. // Checking it now, we would need to remove it if we are testing the form validations... if (!is_numeric($value)) { throw new ExpectationException( 'The points cells should contain numeric values, follow this format: ' . $steptableinfo, $this->getSession() ); } $fieldname = $levelroot . '[score]'; $this->set_rubric_field_value($fieldname, $value, true); // Increase the level by one every 2 cells. $levelnumber++; } } } } } /** * Replaces a value from the specified criterion. You can use it when editing rubrics, to set both name or points. * * @When /^I replace "(?P(?:[^"]|\\")*)" rubric level with "(?P(?:[^"]|\\")*)" in "(?P(?:[^"]|\\")*)" criterion$/ * @throws ElementNotFoundException * @param string $currentvalue * @param string $value * @param string $criterionname */ public function i_replace_rubric_level_with($currentvalue, $value, $criterionname) { $currentvalueliteral = behat_context_helper::escape($currentvalue); $criterionliteral = behat_context_helper::escape($criterionname); $criterionxpath = "//div[@id='rubric-rubric']" . "/descendant::td[contains(concat(' ', normalize-space(@class), ' '), ' description ')]"; // It differs between JS on/off. if ($this->running_javascript()) { $criterionxpath .= "/descendant::span[@class='textvalue'][text()=$criterionliteral]" . "/ancestor::tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]"; } else { $criterionxpath .= "/descendant::textarea[text()=$criterionliteral]" . "/ancestor::tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]"; } $inputxpath = $criterionxpath . "/descendant::input[@type='text'][@value=$currentvalueliteral]"; $textareaxpath = $criterionxpath . "/descendant::textarea[text()=$currentvalueliteral]"; if ($this->running_javascript()) { $spansufix = "/ancestor::div[@class='level-wrapper']" . "/descendant::div[@class='definition']" . "/descendant::span[@class='textvalue']"; // Expanding the level input boxes. $this->execute('behat_general::i_click_on', [ $inputxpath . $spansufix . '|' . $textareaxpath . $spansufix, 'xpath', ]); $this->execute( 'behat_forms::i_set_the_field_with_xpath_to', [ $inputxpath . '|' . $textareaxpath, $value, ] ); } else { $fieldnode = $this->find('xpath', $inputxpath . '|' . $textareaxpath); $this->set_rubric_field_value($fieldnode->getAttribute('name'), $value); } } /** * Grades filling the current page rubric. Set one line per criterion and for each criterion set "| Criterion name | Points | Remark |". * * @When /^I grade by filling the rubric with:$/ * * @throws ExpectationException * @param TableNode $rubric */ public function i_grade_by_filling_the_rubric_with(TableNode $rubric) { $criteria = $rubric->getRowsHash(); $stepusage = '"I grade by filling the rubric with:" step needs you to provide a table where each row is a criterion' . ' and each criterion has 3 different values: | Criterion name | Number of points | Remark text |'; // If running Javascript, ensure we zoom in before filling the grades. if ($this->running_javascript()) { $this->execute('behat_general::click_link', get_string('togglezoom', 'mod_assign')); } // First element -> name, second -> points, third -> Remark. foreach ($criteria as $name => $criterion) { // We only expect the points and the remark, as the criterion name is $name. if (count($criterion) !== 2) { throw new ExpectationException($stepusage, $this->getSession()); } // Numeric value here. $points = $criterion[0]; if (!is_numeric($points)) { throw new ExpectationException($stepusage, $this->getSession()); } // Selecting a value. // When JS is disabled there are radio options, with JS enabled divs. $selectedlevelxpath = $this->get_level_xpath($points); if ($this->running_javascript()) { // Only clicking on the selected level if it was not already selected. $levelnode = $this->find('xpath', $selectedlevelxpath); // Using in_array() as there are only a few elements. if (!$levelnode->hasClass('checked')) { $levelnodexpath = $selectedlevelxpath . "//div[contains(concat(' ', normalize-space(@class), ' '), ' score ')]"; $this->execute('behat_general::i_click_on_in_the', array($levelnodexpath, "xpath_element", $this->escape($name), "table_row") ); } } else { // Getting the name of the field. $radioxpath = $this->get_criterion_xpath($name) . $selectedlevelxpath . "/descendant::input[@type='radio']"; $radionode = $this->find('xpath', $radioxpath); // which will delegate the process to the field type. $radionode->setValue($radionode->getAttribute('value')); } // Setting the remark. // First we need to get the textarea name, then we can set the value. $textarea = $this->get_node_in_container('css_element', 'textarea', 'table_row', $name); $this->execute('behat_forms::i_set_the_field_to', array($textarea->getAttribute('name'), $criterion[1])); } // If running Javascript, then ensure to close zoomed rubric. if ($this->running_javascript()) { $this->execute('behat_general::click_link', get_string('togglezoom', 'mod_assign')); } } /** * Checks that the level was previously selected and the user changed to another level. * * @Then /^the level with "(?P\d+)" points was previously selected for the rubric criterion "(?P(?:[^"]|\\")*)"$/ * @throws ExpectationException * @param string $criterionname * @param int $points * @return void */ public function the_level_with_points_was_previously_selected_for_the_rubric_criterion($points, $criterionname) { $levelxpath = $this->get_criterion_xpath($criterionname) . $this->get_level_xpath($points) . "[contains(concat(' ', normalize-space(@class), ' '), ' currentchecked ')]"; // Works both for JS and non-JS. // - JS: Class -> checked is there when is marked as green. // - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a // grade @class contains checked. $levelxpath .= "[not(contains(concat(' ', normalize-space(@class), ' '), ' checked '))]" . "[not(/descendant::input[@type='radio'][@checked!='checked'])]"; try { $this->find('xpath', $levelxpath); } catch (ElementNotFoundException $e) { throw new ExpectationException('"' . $points . '" points level was not previously selected', $this->getSession()); } } /** * Checks that the level is currently selected. Works both when grading rubrics and viewing graded rubrics. * * @Then /^the level with "(?P\d+)" points is selected for the rubric criterion "(?P(?:[^"]|\\")*)"$/ * @throws ExpectationException * @param string $criterionname * @param int $points * @return void */ public function the_level_with_points_is_selected_for_the_rubric_criterion($points, $criterionname) { $levelxpath = $this->get_criterion_xpath($criterionname) . $this->get_level_xpath($points); // Works both for JS and non-JS. // - JS: Class -> checked is there when is marked as green. // - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a // grade @class contains checked. $levelxpath .= "[" . "contains(concat(' ', normalize-space(@class), ' '), ' checked ')" . " or " . "/descendant::input[@type='radio'][@checked='checked']" . "]"; try { $this->find('xpath', $levelxpath); } catch (ElementNotFoundException $e) { throw new ExpectationException('"' . $points . '" points level is not selected', $this->getSession()); } } /** * Checks that the level is not currently selected. Works both when grading rubrics and viewing graded rubrics. * * @Then /^the level with "(?P\d+)" points is not selected for the rubric criterion "(?P(?:[^"]|\\")*)"$/ * @throws ExpectationException * @param string $criterionname * @param int $points * @return void */ public function the_level_with_points_is_not_selected_for_the_rubric_criterion($points, $criterionname) { $levelxpath = $this->get_criterion_xpath($criterionname) . $this->get_level_xpath($points); // Works both for JS and non-JS. // - JS: Class -> checked is there when is marked as green. // - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a // grade @class contains checked. $levelxpath .= "[not(contains(concat(' ', normalize-space(@class), ' '), ' checked '))]" . "[./descendant::input[@type='radio'][@checked!='checked'] or not(./descendant::input[@type='radio'])]"; try { $this->find('xpath', $levelxpath); } catch (ElementNotFoundException $e) { throw new ExpectationException('"' . $points . '" points level is selected', $this->getSession()); } } /** * Makes a hidden rubric field visible (if necessary) and sets a value on it. * * @param string $name The name of the field * @param string $value The value to set * @param bool $visible * @return void */ protected function set_rubric_field_value($name, $value, $visible = false) { // Fields are hidden by default. if ($this->running_javascript() == true && $visible === false) { $xpath = "//*[@name='$name']/following-sibling::*[contains(concat(' ', normalize-space(@class), ' '), ' plainvalue ')]"; $this->execute('behat_general::i_click_on', [ $xpath, 'xpath', ]); } // Set the value now. $this->execute( 'behat_forms::i_set_the_field_to', [ $name, $value, ] ); } /** * Performs click confirming the action. * * @param NodeElement $node * @return void */ protected function click_and_confirm($node) { // Clicks to perform the action. $this->execute('behat_general::i_click_on', [ $node, 'NodeElement', ]); // Confirms the delete. if ($this->running_javascript()) { $this->execute('behat_general::i_click_on_in_the', [ get_string('yes'), 'button', get_string('confirmation', 'admin'), 'dialogue', ]); } } /** * Returns the xpath representing a selected level. * * It is not including the path to the criterion. * * It is the xpath when grading a rubric or viewing a rubric, * it is not the same xpath when editing a rubric. * * @param int $points * @return string */ protected function get_level_xpath($points) { return "//td[contains(concat(' ', normalize-space(@class), ' '), ' level ')]" . "[./descendant::span[@class='scorevalue'][text()='$points']]"; } /** * Returns the xpath representing the selected criterion. * * It is the xpath when grading a rubric or viewing a rubric, * it is not the same xpath when editing a rubric. * * @param string $criterionname Literal including the criterion name. * @return string */ protected function get_criterion_xpath($criterionname) { $literal = behat_context_helper::escape($criterionname); return "//tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]" . "[./descendant::td[@class='description'][text()=$literal]]"; } } PK'\٧  1form/rubric/tests/behat/reuse_own_rubrics.featurenu[@gradingform @gradingform_rubric Feature: Reuse my rubrics in other activities In order to save time creating duplicated grading forms As a teacher I need to reuse rubrics that I created previously Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | section | idnumber | | assign | Test assignment 1 name | Test assignment 1 description | C1 | 1 | assign1 | | assign | Test assignment 2 name | Test assignment 2 description | C1 | 1 | assign1 | And I am on the "Test assignment 1 name" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | Grading method | Rubric | And I press "Save and return to course" And I go to "Test assignment 1 name" advanced grading definition page And I set the following fields to these values: | Name | Assignment 1 rubric | | Description | Assignment 1 description | And I define the following rubric: | Criterion 1 | Level 11 | 11 | Level 12 | 12 | Level 3 | 13 | | Criterion 2 | Level 21 | 21 | Level 22 | 22 | Level 3 | 23 | | Criterion 3 | Level 31 | 31 | Level 32 | 32 | | | And I press "Save rubric and make it ready" And I am on the "Test assignment 2 name" "assign activity editing" page And I set the following fields to these values: | Grading method | Rubric | And I press "Save and return to course" And I set "Test assignment 2 name" activity to use "Assignment 1 rubric" grading form Then I should see "Ready for use" And I should see "Criterion 1" And I should see "Criterion 2" And I should see "Criterion 3" And I am on "Course 1" course homepage And I go to "Test assignment 1 name" advanced grading definition page And I should see "Criterion 1" And I should see "Criterion 2" And I should see "Criterion 3" @javascript Scenario: A teacher can reuse one of his/her previously created rubrics, with Javascript enabled Scenario: A teacher can reuse one of his/her previously created rubrics, with Javascript disabled PK'\""+form/rubric/tests/behat/edit_rubric.featurenu[@gradingform @gradingform_rubric Feature: Rubrics can be created and edited In order to use and refine rubrics to grade students As a teacher I need to edit previously used rubrics @javascript Scenario: I can use rubrics to grade and edit them later updating students grades Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | activity | assign | | course | C1 | | section | 1 | | name | Test assignment 1 name | | intro | Test assignment description | | assignfeedback_comments_enabled | 1 | | assignfeedback_editpdf_enabled | 1 | | advancedgradingmethod_submissions | rubric | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on When I go to "Test assignment 1 name" advanced grading definition page # Defining a rubric. And I set the following fields to these values: | Name | Assignment 1 rubric | | Description | Rubric test description | And I define the following rubric: | TMP Criterion 1 | TMP Level 11 | 11 | TMP Level 12 | 12 | | TMP Criterion 2 | TMP Level 21 | 21 | TMP Level 22 | 22 | | TMP Criterion 3 | TMP Level 31 | 31 | TMP Level 32 | 32 | | TMP Criterion 4 | TMP Level 41 | 41 | TMP Level 42 | 42 | # Checking that only the last ones are saved. And I define the following rubric: | Criterion 1 | Level 11 | 1 | Level 12 | 20 | Level 13 | 40 | Level 14 | 50 | | Criterion 2 | Level 21 | 10 | Level 22 | 20 | Level 23 | 30 | | | | Criterion 3 | Level 31 | 5 | Level 32 | 20 | | | | | And I press "Save as draft" And I go to "Test assignment 1 name" advanced grading definition page And I click on "Move down" "button" in the "Criterion 1" "table_row" And I press "Save rubric and make it ready" Then I should see "Ready for use" # Grading two students. And I navigate to "Assignment" in current page administration And I change window size to "large" And I go to "Student 1" "Test assignment 1 name" activity advanced grading page And I change window size to "medium" And I grade by filling the rubric with: | Criterion 1 | 50 | Very good | And I press "Save changes" # Checking that it complains if you don't select a level for each criterion. And I should see "Please choose something for each criterion" And I grade by filling the rubric with: | Criterion 1 | 50 | Very good | | Criterion 2 | 10 | Mmmm, you can do it better | | Criterion 3 | 5 | Not good | And I complete the advanced grading form with these values: | Feedback comments | In general... work harder... | # Checking that the user grade is correct. And I should see "65" in the "Student 1" "table_row" # Updating the user grade. And I am on the "Test assignment 1 name" "assign activity" page And I change window size to "large" And I go to "Student 1" "Test assignment 1 name" activity advanced grading page And I change window size to "medium" And I grade by filling the rubric with: | Criterion 1 | 20 | Bad, I changed my mind | | Criterion 2 | 10 | Mmmm, you can do it better | | Criterion 3 | 5 | Not good | #And the level with "50" points was previously selected for the rubric criterion "Criterion 1" #And the level with "20" points is selected for the rubric criterion "Criterion 1" And I save the advanced grading form And I should see "35" in the "Student 1" "table_row" And I log out # Viewing it as a student. And I am on the "Test assignment 1 name" "assign activity" page logged in as student1 And I should see "35" in the ".feedback" "css_element" And I should see "Rubric test description" in the ".feedback" "css_element" And I should see "In general... work harder..." And the level with "10" points is selected for the rubric criterion "Criterion 2" And the level with "20" points is selected for the rubric criterion "Criterion 1" And the level with "5" points is selected for the rubric criterion "Criterion 3" And I log out And I am on the "Course 1" course page logged in as teacher1 # Editing a rubric definition without regrading students. And I go to "Test assignment 1 name" advanced grading definition page And "Save as draft" "button" should not exist And I click on "Move up" "button" in the "Criterion 1" "table_row" And I replace "Level 11" rubric level with "Level 11 edited" in "Criterion 1" criterion And I press "Save" And I should see "You are about to save changes to a rubric that has already been used for grading." And I set the field "menurubricregrade" to "Do not mark for regrade" And I press "Continue" And I log out # Check that the student still sees the grade. And I am on the "Test assignment 1 name" "assign activity" page logged in as student1 And I should see "35" in the ".feedback" "css_element" And the level with "20" points is selected for the rubric criterion "Criterion 1" And I log out # Editing a rubric with significant changes. And I log in as "teacher1" And I am on "Course 1" course homepage And I go to "Test assignment 1 name" advanced grading definition page And I click on "Move down" "button" in the "Criterion 2" "table_row" And I replace "1" rubric level with "60" in "Criterion 1" criterion And I press "Save" And I should see "You are about to save significant changes to a rubric that has already been used for grading. The gradebook value will be unchanged, but the rubric will be hidden from students until their item is regraded." And I press "Continue" And I log out # Check that the student doesn't see the grade. And I am on the "Test assignment 1 name" "assign activity" page logged in as student1 And I should see "35" in the ".feedback" "css_element" And the level with "20" points is not selected for the rubric criterion "Criterion 1" And I log out # Regrade student. And I am on the "Test assignment 1 name" "assign activity" page logged in as teacher1 And I change window size to "large" And I go to "Student 1" "Test assignment 1 name" activity advanced grading page And I change window size to "medium" And I should see "The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade." And I save the advanced grading form And I log out # Check that the student sees the grade again. And I am on the "Test assignment 1 name" "assign activity" page logged in as student1 And I should see "31.82" in the ".feedback" "css_element" And the level with "20" points is not selected for the rubric criterion "Criterion 1" # Hide all rubric info for students And I log out And I am on the "Course 1" course page logged in as teacher1 And I go to "Test assignment 1 name" advanced grading definition page And I set the field "Allow users to preview rubric (otherwise it will only be displayed after grading)" to "" And I set the field "Display rubric description during evaluation" to "" And I set the field "Display rubric description to those being graded" to "" And I set the field "Display points for each level during evaluation" to "" And I set the field "Display points for each level to those being graded" to "" And I press "Save" And I set the field "menurubricregrade" to "Do not mark for regrade" And I press "Continue" And I log out # Students should not see anything. And I am on the "Test assignment 1 name" "assign activity" page logged in as student1 And I should not see "Criterion 1" in the ".submissionstatustable" "css_element" And I should not see "Criterion 2" in the ".submissionstatustable" "css_element" And I should not see "Criterion 3" in the ".submissionstatustable" "css_element" And I should not see "Rubric test description" in the ".feedback" "css_element" PK'\ 8form/rubric/tests/behat/publish_rubric_templates.featurenu[@gradingform @gradingform_rubric Feature: Publish rubrics as templates In order to save time to teachers As a manager I need to publish rubrics and make them available to all teachers Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | manager1 | Manager | 1 | manager1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "activities" exist: | activity | course | idnumber | name | intro | advancedgradingmethod_submissions | | assign | C1 | A1 | Test assignment 1 name | TA1 | rubric | | assign | C1 | A2 | Test assignment 2 name | TA2 | rubric | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "system role assigns" exist: | user | role | contextlevel | reference | | manager1 | manager | System | | And I log in as "manager1" And I am on "Course 1" course homepage And I go to "Test assignment 1 name" advanced grading definition page And I set the following fields to these values: | Name | Assignment 1 rubric | | Description | Assignment 1 description | And I define the following rubric: | Criterion 1 | Level 11 | 11 | Level 12 | 12 | | Criterion 2 | Level 21 | 21 | Level 22 | 22 | And I press "Save rubric and make it ready" When I publish "Test assignment 1 name" grading form definition as a public template And I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I set "Test assignment 2 name" activity to use "Assignment 1 rubric" grading form Then I should see "Advanced grading" And I should see "Criterion 1" And I should see "Assignment 1 description" And I go to "Test assignment 2 name" advanced grading definition page And I should see "Current rubric status" @javascript Scenario: Create a rubric template and reuse it as a teacher, with Javascript enabled Then the field "Description" matches value "Assignment 1 description" And I should see "Criterion 1" And I press "Cancel" Scenario: Create a rubric template and reuse it as a teacher, with Javascript disabled Then the field "Description" matches value "Assignment 1 description" # Trying to avoid pointing by id or name as the code internals may change. And "//table[@class='criteria']//textarea[text()='Criterion 1']" "xpath_element" should exist And I press "Cancel" PK'\C:GG1form/rubric/tests/behat/grade_calculation.featurenu[@gradingform @gradingform_rubric @javascript Feature: Converting rubric score to grades In order to use and refine rubrics to grade students As a teacher I need to be able to use different grade settings Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "scales" exist: | name | scale | | Test scale 1 | Disappointing, Good, Very good, Excellent | Scenario Outline: Convert assignment rubric scores to grades. Given the following "activities" exist: | activity | name | intro | course | idnumber | grade | advancedgradingmethod_submissions | | assign | Test assignment 1 | Test | C1 | assign1 | | rubric | When I log in as "teacher1" And I change window size to "large" And I am on "Course 1" course homepage with editing mode on And I go to "Test assignment 1" advanced grading definition page And I set the following fields to these values: | Name | Assignment 1 rubric | | Description | Rubric test description | | Calculate grade having a minimum score of the minimum achievable grade for the rubric | | And I define the following rubric: | Criterion 1 | Level 11 | 20 | Level 12 | 25 | Level 13 | 40 | Level 14 | 50 | | Criterion 2 | Level 21 | 20 | Level 22 | 25 | Level 23 | 30 | | | | Criterion 3 | Level 31 | 10 | Level 32 | 20 | | | | | And I press "Save rubric and make it ready" And I navigate to "Assignment" in current page administration # Grading a student. And I go to "Student 1" "Test assignment 1" activity advanced grading page And I grade by filling the rubric with: | Criterion 1 | 25 | | | Criterion 2 | 20 | | | Criterion 3 | 10 | | And I save the advanced grading form # Checking that the user grade is correct. And I should see "" in the "student1@example.com" "table_row" And I log out Examples: | grade | lockzeropoints | studentgrade | | 100 | 1 | 55.00 | | 70 | 1 | 38.50 | | Test scale 1 | 1 | Good | | 100 | | 10.00 | | 70 | | 7.00 | | Test scale 1 | | Disappointing | Scenario Outline: Convert forum rubric scores with a scale to grades. Given the following "activities" exist: | activity | name | intro | course | idnumber | grade | advancedgradingmethod_submissions | | forum | Test forum 1 | Test | C1 | forum1 | 100 | rubric | And I change window size to "large" And I am on the "Test forum 1" "forum activity editing" page logged in as teacher1 And I expand all fieldsets And I set the following fields to these values: | Whole forum grading > Type | Scale | | Whole forum grading > Scale | Default competence scale | | Grading method | Rubric | And I press "Save and display" And I go to "Test forum 1" advanced grading definition page # Defining a rubric. And I set the following fields to these values: | Name | Forum 1 rubric | | Description | Rubric test description | And I define the following rubric: | Criterion 1 | Level 11 | 1 | Level 12 | 20 | Level 13 | 40 | Level 14 | 50 | | Criterion 2 | Level 21 | 10 | Level 22 | 20 | Level 23 | 30 | | | | Criterion 3 | Level 31 | 5 | Level 32 | 20 | | | | | And I press "Save rubric and make it ready" When I am on the "Test forum 1" "forum activity" page logged in as teacher1 And I press "Grade users" And I click on " points" "radio" And I click on " points" "radio" And I click on " points" "radio" And I press "Save" And I am on the "Course 1" "grades > Grader report > View" page Then I should see "" in the "student1@example.com" "table_row" Examples: | studentgrade | criterion1 | criterion2 | criterion3 | | Competent | Level 14 50 | Level 23 30 | Level 32 20 | | Not yet competent | Level 11 1 | Level 21 10 | Level 31 5 | PK'\#O O /form/rubric/tests/behat/negative_points.featurenu[@gradingform @gradingform_rubric @javascript Feature: Rubrics can have levels with negative scores In order to use and refine rubrics to grade students As a teacher I need to be able to penalise for very wrong submissions Scenario: Using negative levels in rubrics Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | And the following "scales" exist: | name | scale | | Test scale 1 | Disappointing, Good, Very good, Excellent | And the following "activities" exist: | activity | name | intro | course | idnumber | grade | advancedgradingmethod_submissions | | assign | Test assignment 1 | Test | C1 | assign1 | 100 | rubric | And I change window size to "large" When I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I go to "Test assignment 1" advanced grading definition page And I set the following fields to these values: | Name | Assignment 1 rubric | | Description | Rubric test description | And I define the following rubric: | Criterion 1 | Did not try | -11 | Level 12 | 25 | Level 13 | 40 | Level 14 | 50 | | Criterion 2 | Very bad | -20 | Level 22 | 25 | Level 23 | 30 | | | | Criterion 3 | Level 31 | 10 | Level 32 | 20 | | | | | And I press "Save rubric and make it ready" # Grading a student. And I navigate to "Assignment" in current page administration And I go to "Student 1" "Test assignment 1" activity advanced grading page And I grade by filling the rubric with: | Criterion 1 | 25 | | | Criterion 2 | 30 | | | Criterion 3 | 10 | | And I save the advanced grading form And I am on the "Test assignment 1" "assign activity" page And I go to "Student 2" "Test assignment 1" activity advanced grading page And I grade by filling the rubric with: | Criterion 1 | 25 | | | Criterion 2 | -20 | | | Criterion 3 | 10 | | And I save the advanced grading form And I am on the "Test assignment 1" "assign activity" page And I go to "Student 3" "Test assignment 1" activity advanced grading page And I grade by filling the rubric with: | Criterion 1 | -11 | | | Criterion 2 | -20 | | | Criterion 3 | 10 | | And I save the advanced grading form # Checking that the user grade is correct. And I should see "65.00" in the "student1@example.com" "table_row" And I should see "15.00" in the "student2@example.com" "table_row" And I should see "0.00" in the "student3@example.com" "table_row" And I should not see "-" in the "student3@example.com" "table_row" And I log out PK'\zdM@M@form/rubric/js/rubriceditor.jsnu[M.gradingform_rubriceditor = {'templates' : {}, 'eventhandler' : null, 'name' : null, 'Y' : null}; /** * This function is called for each rubriceditor on page. */ M.gradingform_rubriceditor.init = function(Y, options) { M.gradingform_rubriceditor.name = options.name M.gradingform_rubriceditor.Y = Y M.gradingform_rubriceditor.templates[options.name] = { 'criterion' : options.criteriontemplate, 'level' : options.leveltemplate } M.gradingform_rubriceditor.disablealleditors() Y.on('click', M.gradingform_rubriceditor.clickanywhere, 'body', null) YUI().use('event-touch', function (Y) { Y.one('body').on('touchstart', M.gradingform_rubriceditor.clickanywhere); Y.one('body').on('touchend', M.gradingform_rubriceditor.clickanywhere); }) M.gradingform_rubriceditor.addhandlers() }; // Adds handlers for clicking submit button. This function must be called each time JS adds new elements to html M.gradingform_rubriceditor.addhandlers = function() { var Y = M.gradingform_rubriceditor.Y var name = M.gradingform_rubriceditor.name if (M.gradingform_rubriceditor.eventhandler) M.gradingform_rubriceditor.eventhandler.detach() M.gradingform_rubriceditor.eventhandler = Y.on('click', M.gradingform_rubriceditor.buttonclick, '#rubric-'+name+' input[type=submit]', null); } // switches all input text elements to non-edit mode M.gradingform_rubriceditor.disablealleditors = function() { var Y = M.gradingform_rubriceditor.Y var name = M.gradingform_rubriceditor.name Y.all('#rubric-'+name+' .level').each( function(node) {M.gradingform_rubriceditor.editmode(node, false)} ); Y.all('#rubric-'+name+' .description').each( function(node) {M.gradingform_rubriceditor.editmode(node, false)} ); } // function invoked on each click on the page. If level and/or criterion description is clicked // it switches this element to edit mode. If rubric button is clicked it does nothing so the 'buttonclick' // function is invoked M.gradingform_rubriceditor.clickanywhere = function(e) { if (e.type == 'touchstart') return var el = e.target // if clicked on button - disablecurrenteditor, continue if (el.get('tagName') == 'INPUT' && el.get('type') == 'submit') { return } // else if clicked on level and this level is not enabled - enable it // or if clicked on description and this description is not enabled - enable it var focustb = false while (el && !(el.hasClass('level') || el.hasClass('description'))) { if (el.hasClass('score')) focustb = true el = el.get('parentNode') } if (el) { if (el.one('textarea').hasClass('hiddenelement')) { M.gradingform_rubriceditor.disablealleditors() M.gradingform_rubriceditor.editmode(el, true, focustb) } return } // else disablecurrenteditor M.gradingform_rubriceditor.disablealleditors() } // switch the criterion description or level to edit mode or switch back M.gradingform_rubriceditor.editmode = function(el, editmode, focustb) { var ta = el.one('textarea') if (!editmode && ta.hasClass('hiddenelement')) return; if (editmode && !ta.hasClass('hiddenelement')) return; var pseudotablink = '', taplain = ta.get('parentNode').one('.plainvalue'), tbplain = null, tb = el.one('.score input[type=text]') // add 'plainvalue' next to textarea for description/definition and next to input text field for score (if applicable) if (!taplain) { ta.get('parentNode').append('
'+pseudotablink+' 
') taplain = ta.get('parentNode').one('.plainvalue') taplain.one('.pseudotablink').on('focus', M.gradingform_rubriceditor.clickanywhere) if (tb) { tb.get('parentNode').append(''+pseudotablink+' ') tbplain = tb.get('parentNode').one('.plainvalue') tbplain.one('.pseudotablink').on('focus', M.gradingform_rubriceditor.clickanywhere) } } if (tb && !tbplain) tbplain = tb.get('parentNode').one('.plainvalue') if (!editmode) { // if we need to hide the input fields, copy their contents to plainvalue(s). If description/definition // is empty, display the default text ('Click to edit ...') and add/remove 'empty' CSS class to element var value = ta.get('value') if (value.length) taplain.removeClass('empty') else { value = (el.hasClass('level')) ? M.util.get_string('levelempty', 'gradingform_rubric') : M.util.get_string('criterionempty', 'gradingform_rubric') taplain.addClass('empty') } taplain.one('.textvalue').set('innerHTML', Y.Escape.html(value)); if (tb) tbplain.one('.textvalue').set('innerHTML', Y.Escape.html(tb.get('value'))); // hide/display textarea, textbox and plaintexts taplain.removeClass('hiddenelement') ta.addClass('hiddenelement') if (tb) { tbplain.removeClass('hiddenelement') tb.addClass('hiddenelement') } } else { // if we need to show the input fields, set the width/height for textarea so it fills the cell try { var width = parseFloat(ta.get('parentNode').getComputedStyle('width')), height if (el.hasClass('level')) height = parseFloat(el.getComputedStyle('height')) - parseFloat(el.one('.score').getComputedStyle('height')) else height = parseFloat(ta.get('parentNode').getComputedStyle('height')) ta.setStyle('width', Math.max(width-16,50)+'px') ta.setStyle('height', Math.max(height,20)+'px') } catch (err) { // this browser do not support 'computedStyle', leave the default size of the textbox } // hide/display textarea, textbox and plaintexts taplain.addClass('hiddenelement') ta.removeClass('hiddenelement') if (tb) { tbplain.addClass('hiddenelement') tb.removeClass('hiddenelement') } } // focus the proper input field in edit mode if (editmode) { if (tb && focustb) tb.focus(); else ta.focus() } } // handler for clicking on submit buttons within rubriceditor element. Adds/deletes/rearranges criteria and/or levels on client side M.gradingform_rubriceditor.buttonclick = function(e, confirmed) { var Y = M.gradingform_rubriceditor.Y var name = M.gradingform_rubriceditor.name if (e.target.get('type') != 'submit') return; M.gradingform_rubriceditor.disablealleditors() var chunks = e.target.get('id').split('-'), action = chunks[chunks.length-1] if (chunks[0] != name || chunks[1] != 'criteria') return; var elements_str if (chunks.length>4 || action == 'addlevel') { elements_str = '#rubric-'+name+' #'+name+'-criteria-'+chunks[2]+'-levels .level' } else { elements_str = '#rubric-'+name+' .criterion' } // prepare the id of the next inserted level or criterion var newlevid = 0; var newid = 0; if (action == 'addcriterion' || action == 'addlevel' || action == 'duplicate' ) { newid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .criterion'); newlevid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .level'); } if (chunks.length == 3 && action == 'addcriterion') { // ADD NEW CRITERION var levelsscores = [0], levidx = 1 var parentel = Y.one('#'+name+'-criteria') if (parentel.one('>tbody')) parentel = parentel.one('>tbody') if (parentel.all('.criterion').size()) { var lastcriterion = parentel.all('.criterion').item(parentel.all('.criterion').size()-1).all('.level') for (levidx=0;levidxtbody')) { parentel = parentel.one('>tbody'); } var source = Y.one('#'+name+'-criteria-'+chunks[2]); if (source.all('.level')) { var lastcriterion = source.all('.level'); for (levidx = 0; levidx < lastcriterion.size(); levidx++) { levelsdef[levidx] = lastcriterion.item(levidx).one('.definition .textvalue').get('innerHTML'); } for (levidx = 0; levidx < lastcriterion.size(); levidx++) { levelsscores[levidx] = lastcriterion.item(levidx).one('.score input[type=text]').get('value'); } } for (levidx; levidx < 3; levidx++) { levelsscores[levidx] = parseFloat(levelsscores[levidx-1]) + 1; } var levelsstr = ''; for (levidx = 0; levidx < levelsscores.length; levidx++) { levelsstr += M.gradingform_rubriceditor.templates[name].level .replace(/\{LEVEL-id\}/g, 'NEWID'+(newlevid+levidx)) .replace(/\{LEVEL-score\}/g, levelsscores[levidx]) .replace(/\{LEVEL-definition\}/g, levelsdef[levidx]); } var description = source.one('.description .textvalue'); var newcriterion = M.gradingform_rubriceditor.templates[name].criterion .replace(/\{LEVELS\}/, levelsstr) .replace(/\{CRITERION-description\}/, description.get('innerHTML')); parentel.append(newcriterion.replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, '')); M.gradingform_rubriceditor.assignclasses('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-levels .level'); M.gradingform_rubriceditor.addhandlers(); M.gradingform_rubriceditor.disablealleditors(); M.gradingform_rubriceditor.assignclasses(elements_str); M.gradingform_rubriceditor.editmode(Y.one('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-description-cell'),true); } else if (chunks.length == 6 && action == 'delete') { // DELETE LEVEL if (confirmed) { Y.one('#'+name+'-criteria-'+chunks[2]+'-'+chunks[3]+'-'+chunks[4]).remove() M.gradingform_rubriceditor.assignclasses(elements_str) } else { M.util.js_pending('gradingform_rubriceditor:deleteLevelConfirmation'); require(['core/notification', 'core/str'], function(Notification, Str) { Notification.saveCancelPromise( Str.get_string('confirmation', 'admin'), Str.get_string('confirmdeletelevel', 'gradingform_rubric'), Str.get_string('yes', 'moodle') ).then(function() { M.gradingform_rubriceditor.buttonclick.apply(this, [e, true]); return; }.bind(this)).catch(function() { // User cancelled. }); M.util.js_complete('gradingform_rubriceditor:deleteLevelConfirmation'); }.bind(this)); } } else { // unknown action return; } e.preventDefault(); } // properly set classes (first/last/odd/even), level width and/or criterion sortorder for elements Y.all(elements_str) M.gradingform_rubriceditor.assignclasses = function (elements_str) { var elements = M.gradingform_rubriceditor.Y.all(elements_str) for (var i=0;i. /** * Contains the Guide grading form renderer in all of its glory * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Grading method plugin renderer * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradingform_guide_renderer extends plugin_renderer_base { /** * This function returns html code for displaying criterion. Depending on $mode it may be the * code to edit guide, to preview the guide, to evaluate somebody or to review the evaluation. * * This function may be called from display_guide() to display the whole guide, or it can be * called by itself to return a template used by JavaScript to add new empty criteria to the * guide being designed. * In this case it will use macros like {NAME}, {LEVELS}, {CRITERION-id}, etc. * * When overriding this function it is very important to remember that all elements of html * form (in edit or evaluate mode) must have the name $elementname. * * Also JavaScript relies on the class names of elements and when developer changes them * script might stop working. * * @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller()} * @param array $options An array of options. * showmarkspercriterionstudents (bool) If true adds the current score to the display * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) * @param array $criterion criterion data * @param array $value (only in view mode) teacher's feedback on this criterion * @param array $validationerrors An array containing validation errors to be shown * @param array $comments Array of frequently used comments. * @return string */ public function criterion_template($mode, $options, $elementname = '{NAME}', $criterion = null, $value = null, $validationerrors = null, $comments = null) { if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) { $criterion = array('id' => '{CRITERION-id}', 'description' => '{CRITERION-description}', 'sortorder' => '{CRITERION-sortorder}', 'class' => '{CRITERION-class}', 'descriptionmarkers' => '{CRITERION-descriptionmarkers}', 'shortname' => '{CRITERION-shortname}', 'maxscore' => '{CRITERION-maxscore}'); } else { foreach (array('sortorder', 'description', 'class', 'shortname', 'descriptionmarkers', 'maxscore') as $key) { // Set missing array elements to empty strings to avoid warnings. if (!array_key_exists($key, $criterion)) { $criterion[$key] = ''; } } // Set a default description format if not set. if (!array_key_exists('descriptionformat', $criterion)) { $criterion['descriptionformat'] = FORMAT_MOODLE; } // Set a default description markers format if not set. if (!array_key_exists('descriptionmarkersformat', $criterion)) { $criterion['descriptionmarkersformat'] = FORMAT_MOODLE; } } $criteriontemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $criterion['class'], 'id' => '{NAME}-criteria-{CRITERION-id}')); $descriptionclass = 'description'; if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) { $criteriontemplate .= html_writer::start_tag('td', array('class' => 'controls')); foreach (array('moveup', 'delete', 'movedown') as $key) { $value = get_string('criterion'.$key, 'gradingform_guide'); $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}]['.$key.']', 'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value, 'title' => $value)); $criteriontemplate .= html_writer::tag('div', $button, array('class' => $key)); } $criteriontemplate .= html_writer::end_tag('td'); // Controls. $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder'])); $shortnameinput = html_writer::empty_tag('input', array('type' => 'text', 'name' => '{NAME}[criteria][{CRITERION-id}][shortname]', 'id ' => '{NAME}-criteria-{CRITERION-id}-shortname', 'value' => $criterion['shortname'], 'aria-labelledby' => '{NAME}-criterion-name-label')); $shortname = html_writer::tag('div', $shortnameinput, array('class' => 'criterionname')); $descriptioninput = html_writer::tag('textarea', s($criterion['description']), array('name' => '{NAME}[criteria][{CRITERION-id}][description]', 'id' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '65', 'rows' => '5')); $description = html_writer::tag('div', $descriptioninput, array('class' => 'criteriondesc')); $descriptionmarkersinput = html_writer::tag('textarea', s($criterion['descriptionmarkers']), array('name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'id' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'cols' => '65', 'rows' => '5')); $descriptionmarkers = html_writer::tag('div', $descriptionmarkersinput, array('class' => 'criteriondescmarkers')); $maxscore = html_writer::empty_tag('input', array('type'=> 'text', 'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]', 'size' => '3', 'value' => $criterion['maxscore'], 'id' => '{NAME}[criteria][{CRITERION-id}][maxscore]')); $maxscore = html_writer::tag('div', $maxscore, array('class'=>'criterionmaxscore')); } else { if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN) { $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder'])); $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][shortname]', 'value' => $criterion['shortname'])); $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][description]', 'value' => $criterion['description'])); $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'value' => $criterion['descriptionmarkers'])); $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]', 'value' => $criterion['maxscore'])); } else if ($mode == gradingform_guide_controller::DISPLAY_EVAL || $mode == gradingform_guide_controller::DISPLAY_VIEW) { $descriptionclass = 'descriptionreadonly'; } $shortnameparams = array( 'name' => '{NAME}[criteria][{CRITERION-id}][shortname]', 'id' => '{NAME}[criteria][{CRITERION-id}][shortname]', 'aria-describedby' => '{NAME}-criterion-name-label' ); $shortname = html_writer::div( format_text($criterion['shortname'], FORMAT_HTML), 'criterionshortname', $shortnameparams ); $descmarkerclass = ''; $descstudentclass = ''; if ($mode == gradingform_guide_controller::DISPLAY_EVAL) { if (!get_user_preferences('gradingform_guide-showmarkerdesc', true)) { $descmarkerclass = ' hide'; } if (!get_user_preferences('gradingform_guide-showstudentdesc', true)) { $descstudentclass = ' hide'; } } $description = html_writer::tag('div', format_text($criterion['description'], $criterion['descriptionformat']), array('class'=>'criteriondescription'.$descstudentclass, 'name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]')); $descriptionmarkers = html_writer::tag('div', format_text($criterion['descriptionmarkers'], $criterion['descriptionmarkersformat']), array('class'=>'criteriondescriptionmarkers'.$descmarkerclass, 'name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]')); $maxscore = html_writer::tag('div', s($criterion['maxscore']), array('class'=>'criteriondescriptionscore', 'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]')); // Retain newlines as
tags when displaying the marking guide. $description = nl2br($description); $descriptionmarkers = nl2br($descriptionmarkers); } if (isset($criterion['error_description'])) { $descriptionclass .= ' error'; } $title = $shortname; if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL || $mode == gradingform_guide_controller::DISPLAY_PREVIEW) { $title .= html_writer::tag('label', get_string('descriptionstudents', 'gradingform_guide'), array('for'=>'{NAME}[criteria][{CRITERION-id}][description]')); $title .= $description; $title .= html_writer::tag('label', get_string('descriptionmarkers', 'gradingform_guide'), array('for'=>'{NAME}[criteria][{CRITERION-id}][descriptionmarkers]')); $title .= $descriptionmarkers; $title .= html_writer::tag('label', get_string('maxscore', 'gradingform_guide'), array('for'=>'{NAME}[criteria][{CRITERION-id}][maxscore]')); $title .= $maxscore; } else if ($mode == gradingform_guide_controller::DISPLAY_PREVIEW_GRADED || $mode == gradingform_guide_controller::DISPLAY_VIEW) { $title .= $description; if (!empty($options['showmarkspercriterionstudents'])) { $title .= html_writer::label(get_string('maxscore', 'gradingform_guide'), null); $title .= $maxscore; } } else { $title .= $description . $descriptionmarkers; } // Title cell params. $titletdparams = array( 'class' => $descriptionclass, 'id' => '{NAME}-criteria-{CRITERION-id}-shortname-cell' ); if ($mode != gradingform_guide_controller::DISPLAY_EDIT_FULL && $mode != gradingform_guide_controller::DISPLAY_EDIT_FROZEN) { // Set description's cell as tab-focusable. $titletdparams['tabindex'] = '0'; } $criteriontemplate .= html_writer::tag('td', $title, $titletdparams); $currentremark = ''; $currentscore = ''; if (isset($value['remark'])) { $currentremark = $value['remark']; } if (isset($value['score'])) { $currentscore = $value['score']; } // Element ID of the remark text area. $remarkid = $elementname . '-criteria-' . $criterion['id'] . '-remark'; if ($mode == gradingform_guide_controller::DISPLAY_EVAL) { $scoreclass = ''; if (!empty($validationerrors[$criterion['id']]['score'])) { $scoreclass = 'error'; $currentscore = $validationerrors[$criterion['id']]['score']; // Show invalid score in form. } // Grading remark text area parameters. $remarkparams = array( 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'id' => $remarkid, 'cols' => '65', 'rows' => '5', 'class' => 'markingguideremark form-control', 'aria-labelledby' => '{NAME}-remarklabel{CRITERION-id}' ); // Grading remark text area. $input = html_writer::tag('textarea', s($currentremark), $remarkparams); // Show the frequently-used comments chooser only if there are defined entries. $commentchooser = ''; if (!empty($comments)) { // Frequently used comments chooser. $chooserbuttonid = 'criteria-' . $criterion['id'] . '-commentchooser'; $commentchooserparams = array('id' => $chooserbuttonid, 'class' => 'commentchooser btn btn-secondary'); $commentchooser = html_writer::tag('button', get_string('insertcomment', 'gradingform_guide'), $commentchooserparams); // Option items for the frequently used comments chooser dialog. $commentoptions = array(); foreach ($comments as $id => $comment) { $commentoption = new stdClass(); $commentoption->id = $id; $commentoption->description = html_to_text(format_text($comment['description'], $comment['descriptionformat'])); $commentoptions[] = $commentoption; } // Include string for JS for the comment chooser title. $this->page->requires->string_for_js('insertcomment', 'gradingform_guide'); // Include comment_chooser module. $this->page->requires->js_call_amd('gradingform_guide/comment_chooser', 'initialise', array($criterion['id'], $chooserbuttonid, $remarkid, $commentoptions)); } // Hidden marking guide remark label. $remarklabelparams = array( 'class' => 'hidden', 'id' => '{NAME}-remarklabel{CRITERION-id}' ); $remarklabeltext = get_string('criterionremark', 'gradingform_guide', format_text($criterion['shortname'], FORMAT_HTML)); $remarklabel = html_writer::label($remarklabeltext, $remarkid, false, $remarklabelparams); $criteriontemplate .= html_writer::tag('td', $remarklabel . $input . $commentchooser, array('class' => 'remark')); // Score input and max score. $scoreinputparams = array( 'type' => 'text', 'name' => '{NAME}[criteria][{CRITERION-id}][score]', 'class' => $scoreclass . ' form-control', 'id' => '{NAME}-criteria-{CRITERION-id}-score', 'size' => '3', 'value' => $currentscore, 'aria-labelledby' => '{NAME}-score-label' ); $score = html_writer::empty_tag('input', $scoreinputparams); $score .= html_writer::div('/' . s($criterion['maxscore'])); $criteriontemplate .= html_writer::tag('td', $score, array('class' => 'score')); } else if ($mode == gradingform_guide_controller::DISPLAY_EVAL_FROZEN) { $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark)); } else if ($mode == gradingform_guide_controller::DISPLAY_REVIEW || $mode == gradingform_guide_controller::DISPLAY_VIEW) { // Hidden marking guide remark description. $remarkdescparams = array( 'id' => '{NAME}-criteria-{CRITERION-id}-remark-desc' ); $remarkdesctext = get_string('criterionremark', 'gradingform_guide', $criterion['shortname']); $remarkdesc = html_writer::div($remarkdesctext, 'hidden', $remarkdescparams); // Remarks cell. $remarkdiv = html_writer::div(s($currentremark)); $remarkcellparams = array( 'class' => 'remark', 'tabindex' => '0', 'id' => '{NAME}-criteria-{CRITERION-id}-remark', 'aria-describedby' => '{NAME}-criteria-{CRITERION-id}-remark-desc' ); $criteriontemplate .= html_writer::tag('td', $remarkdesc . $remarkdiv, $remarkcellparams); // Score cell. if (!empty($options['showmarkspercriterionstudents'])) { $scorecellparams = array( 'class' => 'score', 'tabindex' => '0', 'id' => '{NAME}-criteria-{CRITERION-id}-score', 'aria-describedby' => '{NAME}-score-label' ); $scorediv = html_writer::div(s($currentscore) . ' / ' . s($criterion['maxscore'])); $criteriontemplate .= html_writer::tag('td', $scorediv, $scorecellparams); } } $criteriontemplate .= html_writer::end_tag('tr'); // Criterion. $criteriontemplate = str_replace('{NAME}', $elementname, $criteriontemplate); $criteriontemplate = str_replace('{CRITERION-id}', $criterion['id'], $criteriontemplate); return $criteriontemplate; } /** * This function returns html code for displaying criterion. Depending on $mode it may be the * code to edit guide, to preview the guide, to evaluate somebody or to review the evaluation. * * This function may be called from display_guide() to display the whole guide, or it can be * called by itself to return a template used by JavaScript to add new empty criteria to the * guide being designed. * In this case it will use macros like {NAME}, {LEVELS}, {CRITERION-id}, etc. * * When overriding this function it is very important to remember that all elements of html * form (in edit or evaluate mode) must have the name $elementname. * * Also JavaScript relies on the class names of elements and when developer changes them * script might stop working. * * @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller} * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) * @param array $comment * @return string */ public function comment_template($mode, $elementname = '{NAME}', $comment = null) { if ($comment === null || !is_array($comment) || !array_key_exists('id', $comment)) { $comment = array('id' => '{COMMENT-id}', 'description' => '{COMMENT-description}', 'sortorder' => '{COMMENT-sortorder}', 'class' => '{COMMENT-class}'); } else { foreach (array('sortorder', 'description', 'class') as $key) { // Set missing array elements to empty strings to avoid warnings. if (!array_key_exists($key, $comment)) { $criterion[$key] = ''; } } } $commenttemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $comment['class'], 'id' => '{NAME}-comments-{COMMENT-id}')); if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) { $commenttemplate .= html_writer::start_tag('td', array('class' => 'controls')); foreach (array('moveup', 'delete', 'movedown') as $key) { $value = get_string('comments'.$key, 'gradingform_guide'); $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[comments][{COMMENT-id}]['.$key.']', 'id' => '{NAME}-comments-{COMMENT-id}-'.$key, 'value' => $value, 'title' => $value)); $commenttemplate .= html_writer::tag('div', $button, array('class' => $key)); } $commenttemplate .= html_writer::end_tag('td'); // Controls. $commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[comments][{COMMENT-id}][sortorder]', 'value' => $comment['sortorder'])); $description = html_writer::tag('textarea', s($comment['description']), array('name' => '{NAME}[comments][{COMMENT-id}][description]', 'id' => '{NAME}-comments-{COMMENT-id}-description', 'aria-labelledby' => '{NAME}-comment-label', 'cols' => '65', 'rows' => '5')); $description = html_writer::tag('div', $description, array('class'=>'criteriondesc')); } else { if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN) { $commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[comments][{COMMENT-id}][sortorder]', 'value' => $comment['sortorder'])); $commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[comments][{COMMENT-id}][description]', 'value' => $comment['description'])); } if ($mode == gradingform_guide_controller::DISPLAY_EVAL) { $description = html_writer::tag('span', s($comment['description']), array('name' => '{NAME}[comments][{COMMENT-id}][description]', 'title' => get_string('clicktocopy', 'gradingform_guide'), 'id' => '{NAME}[comments][{COMMENT-id}]', 'class'=>'markingguidecomment')); } else { // Set a default description format if not set. if (!array_key_exists('descriptionformat', $comment)) { $comment['descriptionformat'] = FORMAT_MOODLE; } $description = format_text($comment['description'], $comment['descriptionformat']); } // Retain newlines as
tags when displaying 'frequently used comments'. $description = nl2br($description); } $descriptionclass = 'description'; if (isset($comment['error_description'])) { $descriptionclass .= ' error'; } $descriptioncellparams = array( 'class' => $descriptionclass, 'id' => '{NAME}-comments-{COMMENT-id}-description-cell' ); // Make description cell tab-focusable when in review mode. if ($mode != gradingform_guide_controller::DISPLAY_EDIT_FULL && $mode != gradingform_guide_controller::DISPLAY_EDIT_FROZEN) { $descriptioncellparams['tabindex'] = '0'; } $commenttemplate .= html_writer::tag('td', $description, $descriptioncellparams); $commenttemplate .= html_writer::end_tag('tr'); // Criterion. $commenttemplate = str_replace('{NAME}', $elementname, $commenttemplate); $commenttemplate = str_replace('{COMMENT-id}', $comment['id'], $commenttemplate); return $commenttemplate; } /** * This function returns html code for displaying guide template (content before and after * criteria list). Depending on $mode it may be the code to edit guide, to preview the guide, * to evaluate somebody or to review the evaluation. * * This function is called from display_guide() to display the whole guide. * * When overriding this function it is very important to remember that all elements of html * form (in edit or evaluate mode) must have the name $elementname. * * Also JavaScript relies on the class names of elements and when developer changes them * script might stop working. * * @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller} * @param array $options An array of options provided to {@link gradingform_guide_renderer::guide_edit_options()} * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) * @param string $criteriastr evaluated templates for this guide's criteria * @param string $commentstr * @return string */ protected function guide_template($mode, $options, $elementname, $criteriastr, $commentstr) { $classsuffix = ''; // CSS suffix for class of the main div. Depends on the mode. switch ($mode) { case gradingform_guide_controller::DISPLAY_EDIT_FULL: $classsuffix = ' editor editable'; break; case gradingform_guide_controller::DISPLAY_EDIT_FROZEN: $classsuffix = ' editor frozen'; break; case gradingform_guide_controller::DISPLAY_PREVIEW: case gradingform_guide_controller::DISPLAY_PREVIEW_GRADED: $classsuffix = ' editor preview'; break; case gradingform_guide_controller::DISPLAY_EVAL: $classsuffix = ' evaluate editable'; break; case gradingform_guide_controller::DISPLAY_EVAL_FROZEN: $classsuffix = ' evaluate frozen'; break; case gradingform_guide_controller::DISPLAY_REVIEW: $classsuffix = ' review'; break; case gradingform_guide_controller::DISPLAY_VIEW: $classsuffix = ' view'; break; } $guidetemplate = html_writer::start_tag('div', array('id' => 'guide-{NAME}', 'class' => 'clearfix gradingform_guide'.$classsuffix)); // Hidden guide label. $guidedescparams = array( 'id' => 'guide-{NAME}-desc', 'aria-hidden' => 'true' ); $guidetemplate .= html_writer::div(get_string('guide', 'gradingform_guide'), 'hidden', $guidedescparams); // Hidden criterion name label/description. $guidetemplate .= html_writer::div(get_string('criterionname', 'gradingform_guide'), 'hidden', array('id' => '{NAME}-criterion-name-label')); // Hidden score label/description. $guidetemplate .= html_writer::div(get_string('score', 'gradingform_guide'), 'hidden', array('id' => '{NAME}-score-label')); // Criteria table parameters. $criteriatableparams = array( 'class' => 'criteria', 'id' => '{NAME}-criteria', 'aria-describedby' => 'guide-{NAME}-desc'); $guidetemplate .= html_writer::tag('table', $criteriastr, $criteriatableparams); if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) { $value = get_string('addcriterion', 'gradingform_guide'); $input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][addcriterion]', 'id' => '{NAME}-criteria-addcriterion', 'value' => $value, 'title' => $value)); $guidetemplate .= html_writer::tag('div', $input, array('class' => 'addcriterion')); } if (!empty($commentstr)) { $guidetemplate .= html_writer::div(get_string('comments', 'gradingform_guide'), 'commentheader', array('id' => '{NAME}-comments-label')); $guidetemplate .= html_writer::div(get_string('comment', 'gradingform_guide'), 'hidden', array('id' => '{NAME}-comment-label', 'aria-hidden' => 'true')); $commentstableparams = array( 'class' => 'comments', 'id' => '{NAME}-comments', 'aria-describedby' => '{NAME}-comments-label'); $guidetemplate .= html_writer::tag('table', $commentstr, $commentstableparams); } if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) { $value = get_string('addcomment', 'gradingform_guide'); $input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[comments][addcomment]', 'id' => '{NAME}-comments-addcomment', 'value' => $value, 'title' => $value)); $guidetemplate .= html_writer::tag('div', $input, array('class' => 'addcomment')); } $guidetemplate .= $this->guide_edit_options($mode, $options); $guidetemplate .= html_writer::end_tag('div'); return str_replace('{NAME}', $elementname, $guidetemplate); } /** * Generates html template to view/edit the guide options. Expression {NAME} is used in * template for the form element name * * @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller} * @param array $options * @return string */ protected function guide_edit_options($mode, $options) { if ($mode != gradingform_guide_controller::DISPLAY_EDIT_FULL && $mode != gradingform_guide_controller::DISPLAY_EDIT_FROZEN && $mode != gradingform_guide_controller::DISPLAY_PREVIEW) { // Options are displayed only for people who can manage. return; } $html = html_writer::start_tag('div', array('class' => 'options')); $html .= html_writer::tag('div', get_string('guideoptions', 'gradingform_guide'), array('class' => 'optionsheading')); $attrs = array('type' => 'hidden', 'name' => '{NAME}[options][optionsset]', 'value' => 1, 'class' => 'form-control'); $html .= html_writer::empty_tag('input', $attrs); foreach ($options as $option => $value) { $html .= html_writer::start_tag('div', array('class' => 'option '.$option)); $attrs = array('name' => '{NAME}[options]['.$option.']', 'id' => '{NAME}-options-'.$option); switch ($option) { case 'sortlevelsasc': // Display option as dropdown. $html .= html_writer::tag('span', get_string($option, 'gradingform_guide'), array('class' => 'label')); $value = (int)(!!$value); // Make sure $value is either 0 or 1. if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) { $selectoptions = array(0 => get_string($option.'0', 'gradingform_guide'), 1 => get_string($option.'1', 'gradingform_guide')); $valuestr = html_writer::select($selectoptions, $attrs['name'], $value, false, array('id' => $attrs['id'])); $html .= html_writer::tag('span', $valuestr, array('class' => 'value')); // TODO add here button 'Sort levels'. } else { $html .= html_writer::tag('span', get_string($option.$value, 'gradingform_guide'), array('class' => 'value')); if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN) { $html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value)); } } break; default: if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN && $value) { $html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value)); } // Display option as checkbox. $attrs['type'] = 'checkbox'; $attrs['value'] = 1; if ($value) { $attrs['checked'] = 'checked'; } if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN || $mode == gradingform_guide_controller::DISPLAY_PREVIEW) { $attrs['disabled'] = 'disabled'; unset($attrs['name']); } $html .= html_writer::empty_tag('input', $attrs); $html .= html_writer::tag('label', get_string($option, 'gradingform_guide'), array('for' => $attrs['id'])); break; } $html .= html_writer::end_tag('div'); // Option. } $html .= html_writer::end_tag('div'); // Options. return $html; } /** * This function returns html code for displaying guide. Depending on $mode it may be the code * to edit guide, to preview the guide, to evaluate somebody or to review the evaluation. * * It is very unlikely that this function needs to be overriden by theme. It does not produce * any html code, it just prepares data about guide design and evaluation, adds the CSS * class to elements and calls the functions level_template, criterion_template and * guide_template * * @param array $criteria data about the guide design * @param array $comments * @param array $options * @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller} * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) * @param array $values evaluation result * @param array $validationerrors * @return string */ public function display_guide($criteria, $comments, $options, $mode, $elementname = null, $values = null, $validationerrors = null) { $criteriastr = ''; $cnt = 0; foreach ($criteria as $id => $criterion) { $criterion['class'] = $this->get_css_class_suffix($cnt++, count($criteria) -1); $criterion['id'] = $id; if (isset($values['criteria'][$id])) { $criterionvalue = $values['criteria'][$id]; } else { $criterionvalue = null; } $criteriastr .= $this->criterion_template($mode, $options, $elementname, $criterion, $criterionvalue, $validationerrors, $comments); } $cnt = 0; $commentstr = ''; // Check if comments should be displayed. if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL || $mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN || $mode == gradingform_guide_controller::DISPLAY_PREVIEW || $mode == gradingform_guide_controller::DISPLAY_EVAL_FROZEN) { foreach ($comments as $id => $comment) { $comment['id'] = $id; $comment['class'] = $this->get_css_class_suffix($cnt++, count($comments) -1); $commentstr .= $this->comment_template($mode, $elementname, $comment); } } $output = $this->guide_template($mode, $options, $elementname, $criteriastr, $commentstr); if ($mode == gradingform_guide_controller::DISPLAY_EVAL) { $showdesc = get_user_preferences('gradingform_guide-showmarkerdesc', true); $showdescstud = get_user_preferences('gradingform_guide-showstudentdesc', true); $checked1 = array(); $checked2 = array(); $checked_s1 = array(); $checked_s2 = array(); $checked = array('checked' => 'checked'); if ($showdesc) { $checked1 = $checked; } else { $checked2 = $checked; } if ($showdescstud) { $checked_s1 = $checked; } else { $checked_s2 = $checked; } $radio1 = html_writer::tag('input', get_string('showmarkerdesc', 'gradingform_guide'), array('type' => 'radio', 'name' => 'showmarkerdesc', 'value' => "true")+$checked1); $radio1 = html_writer::tag('label', $radio1); $radio2 = html_writer::tag('input', get_string('hidemarkerdesc', 'gradingform_guide'), array('type' => 'radio', 'name' => 'showmarkerdesc', 'value' => "false")+$checked2); $radio2 = html_writer::tag('label', $radio2); $output .= html_writer::tag('div', $radio1 . $radio2, array('class' => 'showmarkerdesc')); $radio1 = html_writer::tag('input', get_string('showstudentdesc', 'gradingform_guide'), array('type' => 'radio', 'name' => 'showstudentdesc', 'value' => "true")+$checked_s1); $radio1 = html_writer::tag('label', $radio1); $radio2 = html_writer::tag('input', get_string('hidestudentdesc', 'gradingform_guide'), array('type' => 'radio', 'name' => 'showstudentdesc', 'value' => "false")+$checked_s2); $radio2 = html_writer::tag('label', $radio2); $output .= html_writer::tag('div', $radio1 . $radio2, array('class' => 'showstudentdesc')); } return $output; } /** * Help function to return CSS class names for element (first/last/even/odd) with leading space * * @param int $idx index of this element in the row/column * @param int $maxidx maximum index of the element in the row/column * @return string */ protected function get_css_class_suffix($idx, $maxidx) { $class = ''; if ($idx == 0) { $class .= ' first'; } if ($idx == $maxidx) { $class .= ' last'; } if ($idx % 2) { $class .= ' odd'; } else { $class .= ' even'; } return $class; } /** * Displays for the student the list of instances or default content if no instances found * * @param array $instances array of objects of type gradingform_guide_instance * @param string $defaultcontent default string that would be displayed without advanced grading * @param bool $cangrade whether current user has capability to grade in this context * @return string */ public function display_instances($instances, $defaultcontent, $cangrade) { $return = ''; if (count($instances)) { $return .= html_writer::start_tag('div', array('class' => 'advancedgrade')); $idx = 0; foreach ($instances as $instance) { $return .= $this->display_instance($instance, $idx++, $cangrade); } $return .= html_writer::end_tag('div'); } return $return. $defaultcontent; } /** * Displays one grading instance * * @param gradingform_guide_instance $instance * @param int $idx unique number of instance on page * @param bool $cangrade whether current user has capability to grade in this context */ public function display_instance(gradingform_guide_instance $instance, $idx, $cangrade) { $criteria = $instance->get_controller()->get_definition()->guide_criteria; $options = $instance->get_controller()->get_options(); $values = $instance->get_guide_filling(); if ($cangrade) { $mode = gradingform_guide_controller::DISPLAY_REVIEW; } else { $mode = gradingform_guide_controller::DISPLAY_VIEW; } $output = $this->box($instance->get_controller()->get_formatted_description(), 'gradingform_guide-description'). $this->display_guide($criteria, array(), $options, $mode, 'guide'.$idx, $values); return $output; } /** * Displays a confirmation message after a regrade has occured * * @param string $elementname * @param int $changelevel * @param int $value The regrade option that was used * @return string */ public function display_regrade_confirmation($elementname, $changelevel, $value) { $html = html_writer::start_tag('div', array('class' => 'gradingform_guide-regrade', 'role' => 'alert')); if ($changelevel<=2) { $html .= get_string('regrademessage1', 'gradingform_guide'); $selectoptions = array( 0 => get_string('regradeoption0', 'gradingform_guide'), 1 => get_string('regradeoption1', 'gradingform_guide') ); $html .= html_writer::select($selectoptions, $elementname.'[regrade]', $value, false); } else { $html .= get_string('regrademessage5', 'gradingform_guide'); $html .= html_writer::empty_tag('input', array('name' => $elementname.'[regrade]', 'value' => 1, 'type' => 'hidden')); } $html .= html_writer::end_tag('div'); return $html; } /** * Generates and returns HTML code to display information box about how guide score is converted to the grade * * @param array $scores * @return string */ public function display_guide_mapping_explained($scores) { $html = ''; if (!$scores) { return $html; } if (isset($scores['modulegrade']) && $scores['maxscore'] != $scores['modulegrade']) { $html .= $this->box(html_writer::tag('div', get_string('guidemappingexplained', 'gradingform_guide', (object)$scores)) , 'generalbox gradingform_guide-error'); } return $html; } } PK'\B ssform/guide/db/services.phpnu[. /** * External functions and service definitions for the Marking Guide advanced grading form. * * @package gradingform_guide * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $functions = [ 'gradingform_guide_grader_gradingpanel_fetch' => [ 'classname' => 'gradingform_guide\\grades\\grader\\gradingpanel\\external\\fetch', 'description' => 'Fetch the data required to display the grader grading panel, ' . 'creating the grade item if required', 'type' => 'write', 'ajax' => true, ], 'gradingform_guide_grader_gradingpanel_store' => [ 'classname' => 'gradingform_guide\\grades\\grader\\gradingpanel\\external\\store', 'description' => 'Store the grading data for a user from the grader grading panel.', 'type' => 'write', 'ajax' => true, ], ]; PK'\tIform/guide/db/upgrade.phpnu[. /** * This file keeps track of upgrades to the marking guide grading method. * * @package gradingform_guide * @category upgrade * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Marking guide grading method upgrade task. * * @param int $oldversion The version we are upgrading form. * @return bool Returns true on success. * @throws coding_exception * @throws downgrade_exception * @throws upgrade_exception */ function xmldb_gradingform_guide_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; } PK'\??form/guide/db/install.xmlnu[
PK'\󃐮(form/guide/lang/en/gradingform_guide.phpnu[. /** * Strings for the marking guide advanced grading plugin * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['addcomment'] = 'Add frequently used comment'; $string['additionalcomments'] = 'Additional comments'; $string['additionalcommentsforcriterion'] = 'Additional comments for criterion, {$a}'; $string['addcriterion'] = 'Add criterion'; $string['alwaysshowdefinition'] = 'Show guide definition to students'; $string['backtoediting'] = 'Back to editing'; $string['clicktocopy'] = 'Click to copy this text into the criteria feedback'; $string['clicktoedit'] = 'Click to edit'; $string['clicktoeditname'] = 'Click to edit criterion name'; $string['comment'] = 'Comment'; $string['commentpickerforcriterion'] = 'Frequently used comments picker for {$a} additional comments'; $string['comments'] = 'Frequently used comments'; $string['commentsdelete'] = 'Delete comment'; $string['commentsempty'] = 'Click to edit comment'; $string['commentsmovedown'] = 'Move down'; $string['commentsmoveup'] = 'Move up'; $string['confirmdeletecriterion'] = 'Are you sure you want to delete this item?'; $string['confirmdeletelevel'] = 'Are you sure you want to delete this level?'; $string['criterion'] = 'Criterion name'; $string['criteriondelete'] = 'Delete criterion'; $string['criterionempty'] = 'Click to edit criterion'; $string['criterionmovedown'] = 'Move down'; $string['criterionmoveup'] = 'Move up'; $string['criterionname'] = 'Criterion name'; $string['criterionremark'] = '{$a} criterion remark'; $string['definemarkingguide'] = 'Define marking guide'; $string['description'] = 'Description'; $string['descriptionmarkers'] = 'Description for Markers'; $string['descriptionstudents'] = 'Description for students'; $string['err_maxscoreisnegative'] = 'The max score is not valid, negative values are not allowed'; $string['err_maxscorenotnumeric'] = 'Criterion max score must be numeric'; $string['err_nocomment'] = 'Comment can not be empty'; $string['err_nodescription'] = 'Student description can not be empty'; $string['err_nodescriptionmarkers'] = 'Marker description can not be empty'; $string['err_nomaxscore'] = 'Criterion max score can not be empty'; $string['err_noshortname'] = 'Criterion name can not be empty'; $string['err_shortnametoolong'] = 'Criterion name must be less than 256 characters'; $string['err_scoreinvalid'] = 'The score given to \'{$a->criterianame}\' is not valid, the max score is: {$a->maxscore}'; $string['err_scoreisnegative'] = 'The score given to \'{$a->criterianame}\' is not valid, negative values are not allowed'; $string['gradingof'] = '{$a} grading'; $string['guide'] = 'Marking guide'; $string['guidemappingexplained'] = 'WARNING: Your marking guide has a maximum grade of {$a->maxscore} points but the maximum grade set in your activity is {$a->modulegrade} The maximum score set in your marking guide will be scaled to the maximum grade in the module.
Intermediate scores will be converted respectively and rounded to the nearest available grade.'; $string['guidenotcompleted'] = 'Please provide a valid grade for each criterion'; $string['guideoptions'] = 'Marking guide options'; $string['guidestatus'] = 'Current marking guide status'; $string['hidemarkerdesc'] = 'Hide marker criterion descriptions'; $string['hidestudentdesc'] = 'Hide student criterion descriptions'; $string['informationforcriterion'] = '{$a} information'; $string['insertcomment'] = 'Insert frequently used comment'; $string['maxscore'] = 'Maximum score'; $string['name'] = 'Name'; $string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.'; $string['outof'] = 'Score out of {$a}'; $string['pluginname'] = 'Marking guide'; $string['previewmarkingguide'] = 'Preview marking guide'; $string['privacy:metadata:criterionid'] = 'An identifier to a criterion for advanced marking.'; $string['privacy:metadata:fillingssummary'] = 'Stores information about a user\'s grade and feedback for the marking guide.'; $string['privacy:metadata:instanceid'] = 'An identifier to a grade used by an activity.'; $string['privacy:metadata:preference:showmarkerdesc'] = 'Whether to show marker criterion descriptions'; $string['privacy:metadata:preference:showstudentdesc'] = 'Whether to show student criterion descriptions'; $string['privacy:metadata:remark'] = 'Remarks related to this grade criterion.'; $string['privacy:metadata:score'] = 'A score for this grade criterion.'; $string['regrademessage1'] = 'You are about to save changes to a marking guide that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the marking guide will be hidden from students until their item is regraded.'; $string['regrademessage5'] = 'You are about to save significant changes to a marking guide that has already been used for grading. The gradebook value will be unchanged, but the marking guide will be hidden from students until their item is regraded.'; $string['regradeoption0'] = 'Do not mark for regrade'; $string['regradeoption1'] = 'Mark for regrade'; $string['remark_help'] = 'Enter any additional comments about this criterion.'; $string['restoredfromdraft'] = 'NOTE: The last attempt to grade this person was not saved properly so draft grades have been restored. If you want to cancel these changes use the \'Cancel\' button below.'; $string['save'] = 'Save'; $string['saveguide'] = 'Save marking guide and make it ready'; $string['saveguidedraft'] = 'Save as draft'; $string['score'] = 'score'; $string['scoreforcriterion'] = '{$a} score'; $string['score_help'] = 'Enter a score for {$a->criterion} between 0 and {$a->maxscore}.'; $string['showmarkerdesc'] = 'Show marker criterion descriptions'; $string['showmarkspercriterionstudents'] = 'Show marks per criterion to students'; $string['showstudentdesc'] = 'Show student criterion descriptions'; PK'\hDform/guide/backup/moodle2/restore_gradingform_guide_plugin.class.phpnu[. /** * Support for restore API * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Restores the marking guide specific data from grading.xml file * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_gradingform_guide_plugin extends restore_gradingform_plugin { /** * Declares the marking guide XML paths attached to the form definition element * * @return array of {@link restore_path_element} */ protected function define_definition_plugin_structure() { $paths = array(); $paths[] = new restore_path_element('gradingform_guide_criterion', $this->get_pathfor('/guidecriteria/guidecriterion')); $paths[] = new restore_path_element('gradingform_guide_comment', $this->get_pathfor('/guidecomments/guidecomment')); // MDL-37714: Correctly locate frequent used comments in both the // current and incorrect old format. $paths[] = new restore_path_element('gradingform_guide_comment_legacy', $this->get_pathfor('/guidecriteria/guidecomments/guidecomment')); return $paths; } /** * Declares the marking guide XML paths attached to the form instance element * * @return array of {@link restore_path_element} */ protected function define_instance_plugin_structure() { $paths = array(); $paths[] = new restore_path_element('gradinform_guide_filling', $this->get_pathfor('/fillings/filling')); return $paths; } /** * Processes criterion element data * * Sets the mapping 'gradingform_guide_criterion' to be used later by * {@link self::process_gradinform_guide_filling()} * * @param array|stdClass $data */ public function process_gradingform_guide_criterion($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->definitionid = $this->get_new_parentid('grading_definition'); $newid = $DB->insert_record('gradingform_guide_criteria', $data); $this->set_mapping('gradingform_guide_criterion', $oldid, $newid); } /** * Processes comments element data * * @param array|stdClass $data The data to insert as a comment */ public function process_gradingform_guide_comment($data) { global $DB; $data = (object)$data; $data->definitionid = $this->get_new_parentid('grading_definition'); $DB->insert_record('gradingform_guide_comments', $data); } /** * Processes comments element data * * @param array|stdClass $data The data to insert as a comment */ public function process_gradingform_guide_comment_legacy($data) { global $DB; $data = (object)$data; $data->definitionid = $this->get_new_parentid('grading_definition'); $DB->insert_record('gradingform_guide_comments', $data); } /** * Processes filling element data * * @param array|stdClass $data The data to insert as a filling */ public function process_gradinform_guide_filling($data) { global $DB; $data = (object)$data; $data->instanceid = $this->get_new_parentid('grading_instance'); $data->criterionid = $this->get_mappingid('gradingform_guide_criterion', $data->criterionid); $DB->insert_record('gradingform_guide_fillings', $data); } } PK'\HuCform/guide/backup/moodle2/backup_gradingform_guide_plugin.class.phpnu[. /** * Support for backup API * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Defines marking guide backup structures * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_gradingform_guide_plugin extends backup_gradingform_plugin { /** * Declares marking guide structures to append to the grading form definition * @return backup_plugin_element */ protected function define_definition_plugin_structure() { // Append data only if the grand-parent element has 'method' set to 'guide'. $plugin = $this->get_plugin_element(null, '../../method', 'guide'); // Create a visible container for our data. $pluginwrapper = new backup_nested_element($this->get_recommended_name()); // Connect our visible container to the parent. $plugin->add_child($pluginwrapper); // Define our elements. $criteria = new backup_nested_element('guidecriteria'); $criterion = new backup_nested_element('guidecriterion', array('id'), array( 'sortorder', 'shortname', 'description', 'descriptionformat', 'descriptionmarkers', 'descriptionmarkersformat', 'maxscore')); $comments = new backup_nested_element('guidecomments'); $comment = new backup_nested_element('guidecomment', array('id'), array( 'sortorder', 'description', 'descriptionformat')); // Build elements hierarchy. $pluginwrapper->add_child($criteria); $criteria->add_child($criterion); $pluginwrapper->add_child($comments); $comments->add_child($comment); // Set sources to populate the data. $criterion->set_source_table('gradingform_guide_criteria', array('definitionid' => backup::VAR_PARENTID)); $comment->set_source_table('gradingform_guide_comments', array('definitionid' => backup::VAR_PARENTID)); // No need to annotate ids or files yet (one day when criterion definition supports // embedded files, they must be annotated here). return $plugin; } /** * Declares marking guide structures to append to the grading form instances * @return backup_plugin_element */ protected function define_instance_plugin_structure() { // Append data only if the ancestor 'definition' element has 'method' set to 'guide'. $plugin = $this->get_plugin_element(null, '../../../../method', 'guide'); // Create a visible container for our data. $pluginwrapper = new backup_nested_element($this->get_recommended_name()); // Connect our visible container to the parent. $plugin->add_child($pluginwrapper); // Define our elements. $fillings = new backup_nested_element('fillings'); $filling = new backup_nested_element('filling', array('id'), array( 'criterionid', 'remark', 'remarkformat', 'score')); // Build elements hierarchy. $pluginwrapper->add_child($fillings); $fillings->add_child($filling); // Set sources to populate the data. $filling->set_source_table('gradingform_guide_fillings', array('instanceid' => backup::VAR_PARENTID)); // No need to annotate ids or files yet (one day when remark field supports // embedded fileds, they must be annotated here). return $plugin; } } PK'\i#i#form/guide/edit_form.phpnu[. /** * The form used at the guide editor page is defined here * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/lib/formslib.php'); require_once(__DIR__.'/guideeditor.php'); MoodleQuickForm::registerElementType('guideeditor', $CFG->dirroot.'/grade/grading/form/guide/guideeditor.php', 'moodlequickform_guideeditor'); /** * Defines the guide edit form * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradingform_guide_editguide extends moodleform { /** * Form element definition */ public function definition() { $form = $this->_form; $form->addElement('hidden', 'areaid'); $form->setType('areaid', PARAM_INT); $form->addElement('hidden', 'returnurl'); $form->setType('returnurl', PARAM_LOCALURL); // Name. $form->addElement('text', 'name', get_string('name', 'gradingform_guide'), array('size' => 52, 'maxlength' => 255)); $form->addRule('name', get_string('required'), 'required', null, 'client'); $form->setType('name', PARAM_TEXT); $form->addRule('name', null, 'maxlength', 255, 'client'); // Description. $options = gradingform_guide_controller::description_form_field_options($this->_customdata['context']); $form->addElement('editor', 'description_editor', get_string('description'), null, $options); $form->setType('description_editor', PARAM_RAW); // Guide completion status. $choices = array(); $choices[gradingform_controller::DEFINITION_STATUS_DRAFT] = html_writer::tag('span', get_string('statusdraft', 'core_grading'), array('class' => 'status draft')); $choices[gradingform_controller::DEFINITION_STATUS_READY] = html_writer::tag('span', get_string('statusready', 'core_grading'), array('class' => 'status ready')); $form->addElement('select', 'status', get_string('guidestatus', 'gradingform_guide'), $choices)->freeze(); // Guide editor. $element = $form->addElement('guideeditor', 'guide', get_string('pluginname', 'gradingform_guide')); $form->setType('guide', PARAM_RAW); $buttonarray = array(); $buttonarray[] = &$form->createElement('submit', 'saveguide', get_string('saveguide', 'gradingform_guide')); if ($this->_customdata['allowdraft']) { $buttonarray[] = &$form->createElement('submit', 'saveguidedraft', get_string('saveguidedraft', 'gradingform_guide')); } $editbutton = &$form->createElement('submit', 'editguide', ' '); $editbutton->freeze(); $buttonarray[] = &$editbutton; $buttonarray[] = &$form->createElement('cancel'); $form->addGroup($buttonarray, 'buttonar', '', array(' '), false); $form->closeHeaderBefore('buttonar'); } /** * Setup the form depending on current values. This method is called after definition(), * data submission and set_data(). * All form setup that is dependent on form values should go in here. * * We remove the element status if there is no current status (i.e. guide is only being created) * so the users do not get confused */ public function definition_after_data() { $form = $this->_form; $el = $form->getElement('status'); if (!$el->getValue()) { $form->removeElement('status'); } else { $vals = array_values($el->getValue()); if ($vals[0] == gradingform_controller::DEFINITION_STATUS_READY) { $this->findbutton('saveguide')->setValue(get_string('save', 'gradingform_guide')); } } } /** * Form vlidation. * If there are errors return array of errors ("fieldname"=>"error message"), * otherwise true if ok. * * @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, * or an empty array if everything is OK (true allowed for backwards compatibility too). */ public function validation($data, $files) { $err = parent::validation($data, $files); $err = array(); $form = $this->_form; $guideel = $form->getElement('guide'); if ($guideel->non_js_button_pressed($data['guide'])) { // If JS is disabled and button such as 'Add criterion' is pressed - prevent from submit. $err['guidedummy'] = 1; } else if (isset($data['editguide'])) { // Continue editing. $err['guidedummy'] = 1; } else if ((isset($data['saveguide']) && $data['saveguide']) || (isset($data['saveguidedraft']) && $data['saveguidedraft'])) { // If user attempts to make guide active - it needs to be validated. if ($guideel->validate($data['guide']) !== false) { $err['guidedummy'] = 1; } } return $err; } /** * Return submitted data if properly submitted or returns NULL if validation fails or * if there is no submitted data. * * @return object submitted data; NULL if not valid or not submitted or cancelled */ public function get_data() { $data = parent::get_data(); if (!empty($data->saveguide)) { $data->status = gradingform_controller::DEFINITION_STATUS_READY; } else if (!empty($data->saveguidedraft)) { $data->status = gradingform_controller::DEFINITION_STATUS_DRAFT; } return $data; } /** * Check if there are changes in the guide and it is needed to ask user whether to * mark the current grades for re-grading. User may confirm re-grading and continue, * return to editing or cancel the changes * * @param gradingform_guide_controller $controller */ public function need_confirm_regrading($controller) { $data = $this->get_data(); if (isset($data->guide['regrade'])) { // We have already displayed the confirmation on the previous step. return false; } if (!isset($data->saveguide) || !$data->saveguide) { // We only need confirmation when button 'Save guide' is pressed. return false; } if (!$controller->has_active_instances()) { // Nothing to re-grade, confirmation not needed. return false; } $changelevel = $controller->update_or_check_guide($data); if ($changelevel == 0) { // No changes in the guide, no confirmation needed. return false; } // Freeze form elements and pass the values in hidden fields. // TODO description_editor does not freeze the normal way! $form = $this->_form; foreach (array('guide', 'name'/*, 'description_editor'*/) as $fieldname) { $el =& $form->getElement($fieldname); $el->freeze(); $el->setPersistantFreeze(true); if ($fieldname == 'guide') { $el->add_regrade_confirmation($changelevel); } } // Replace button text 'saveguide' and unfreeze 'Back to edit' button. $this->findbutton('saveguide')->setValue(get_string('continue')); $el =& $this->findbutton('editguide'); $el->setValue(get_string('backtoediting', 'gradingform_guide')); $el->unfreeze(); return true; } /** * Returns a form element (submit button) with the name $elementname * * @param string $elementname * @return HTML_QuickForm_element */ protected function &findbutton($elementname) { $form = $this->_form; $buttonar =& $form->getElement('buttonar'); $elements =& $buttonar->getElements(); foreach ($elements as $el) { if ($el->getName() == $elementname) { return $el; } } return null; } } PK'\OYform/guide/pix/icon.svgnu[ ]>PK'\v~+Eform/guide/pix/plus.svgnu[ PK'\ƐG  form/guide/pix/info.svgnu[ PK'\4form/guide/pix/icon.pngnu[PNG  IHDRabKGDHIDAT8 0 /,Th R$k0y 0Rkw_y] /@6̉;W=KIENDB`PK'\i2Fform/guide/lib.phpnu[. /** * Grading method controller for the guide plugin * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_external\external_format_value; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/grade/grading/form/lib.php'); /** guide: Used to compare our gradeitem_type against. */ const MARKING_GUIDE = 'guide'; /** * This controller encapsulates the guide grading logic * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradingform_guide_controller extends gradingform_controller { // Modes of displaying the guide (used in gradingform_guide_renderer). /** guide display mode: For editing (moderator or teacher creates a guide) */ const DISPLAY_EDIT_FULL = 1; /** guide display mode: Preview the guide design with hidden fields */ const DISPLAY_EDIT_FROZEN = 2; /** guide display mode: Preview the guide design (for person with manage permission) */ const DISPLAY_PREVIEW = 3; /** guide display mode: Preview the guide (for people being graded) */ const DISPLAY_PREVIEW_GRADED= 8; /** guide display mode: For evaluation, enabled (teacher grades a student) */ const DISPLAY_EVAL = 4; /** guide display mode: For evaluation, with hidden fields */ const DISPLAY_EVAL_FROZEN = 5; /** guide display mode: Teacher reviews filled guide */ const DISPLAY_REVIEW = 6; /** guide display mode: Dispaly filled guide (i.e. students see their grades) */ const DISPLAY_VIEW = 7; /** @var stdClass|false the definition structure */ protected $moduleinstance = false; /** * Extends the module settings navigation with the guide grading settings * * This function is called when the context for the page is an activity module with the * FEATURE_ADVANCED_GRADING, the user has the permission moodle/grade:managegradingforms * and there is an area with the active grading method set to 'guide'. * * @param settings_navigation $settingsnav {@link settings_navigation} * @param navigation_node $node {@link navigation_node} */ public function extend_settings_navigation(settings_navigation $settingsnav, ?navigation_node $node=null) { $node->add(get_string('definemarkingguide', 'gradingform_guide'), $this->get_editor_url(), settings_navigation::TYPE_CUSTOM, null, null, new pix_icon('icon', '', 'gradingform_guide')); } /** * Extends the module navigation * * This function is called when the context for the page is an activity module with the * FEATURE_ADVANCED_GRADING and there is an area with the active grading method set to the given plugin. * * @param global_navigation $navigation {@link global_navigation} * @param navigation_node $node {@link navigation_node} * @return void */ public function extend_navigation(global_navigation $navigation, ?navigation_node $node=null) { if (has_capability('moodle/grade:managegradingforms', $this->get_context())) { // No need for preview if user can manage forms, he will have link to manage.php in settings instead. return; } if ($this->is_form_defined() && ($options = $this->get_options()) && !empty($options['alwaysshowdefinition'])) { $node->add(get_string('gradingof', 'gradingform_guide', get_grading_manager($this->get_areaid())->get_area_title()), new moodle_url('/grade/grading/form/'.$this->get_method_name().'/preview.php', array('areaid' => $this->get_areaid())), settings_navigation::TYPE_CUSTOM); } } /** * Saves the guide definition into the database * * @see parent::update_definition() * @param stdClass $newdefinition guide definition data as coming from gradingform_guide_editguide::get_data() * @param int $usermodified optional userid of the author of the definition, defaults to the current user */ public function update_definition(stdClass $newdefinition, $usermodified = null) { $this->update_or_check_guide($newdefinition, $usermodified, true); if (isset($newdefinition->guide['regrade']) && $newdefinition->guide['regrade']) { $this->mark_for_regrade(); } } /** * Either saves the guide definition into the database or check if it has been changed. * * Returns the level of changes: * 0 - no changes * 1 - only texts or criteria sortorders are changed, students probably do not require re-grading * 2 - added levels but maximum score on guide is the same, students still may not require re-grading * 3 - removed criteria or changed number of points, students require re-grading but may be re-graded automatically * 4 - removed levels - students require re-grading and not all students may be re-graded automatically * 5 - added criteria - all students require manual re-grading * * @param stdClass $newdefinition guide definition data as coming from gradingform_guide_editguide::get_data() * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user * @param bool $doupdate if true actually updates DB, otherwise performs a check * @return int */ public function update_or_check_guide(stdClass $newdefinition, $usermodified = null, $doupdate = false) { global $DB; // Firstly update the common definition data in the {grading_definition} table. if ($this->definition === false) { if (!$doupdate) { // If we create the new definition there is no such thing as re-grading anyway. return 5; } // If definition does not exist yet, create a blank one // (we need id to save files embedded in description). parent::update_definition(new stdClass(), $usermodified); parent::load_definition(); } if (!isset($newdefinition->guide['options'])) { $newdefinition->guide['options'] = self::get_default_options(); } $newdefinition->options = json_encode($newdefinition->guide['options']); $editoroptions = self::description_form_field_options($this->get_context()); $newdefinition = file_postupdate_standard_editor($newdefinition, 'description', $editoroptions, $this->get_context(), 'grading', 'description', $this->definition->id); // Reload the definition from the database. $currentdefinition = $this->get_definition(true); // Update guide data. $haschanges = array(); if (empty($newdefinition->guide['criteria'])) { $newcriteria = array(); } else { $newcriteria = $newdefinition->guide['criteria']; // New ones to be saved. } $currentcriteria = $currentdefinition->guide_criteria; $criteriafields = array('sortorder', 'description', 'descriptionformat', 'descriptionmarkers', 'descriptionmarkersformat', 'shortname', 'maxscore'); foreach ($newcriteria as $id => $criterion) { if (preg_match('/^NEWID\d+$/', $id)) { // Insert criterion into DB. $data = array('definitionid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE, 'descriptionmarkersformat' => FORMAT_MOODLE); // TODO format is not supported yet. foreach ($criteriafields as $key) { if (array_key_exists($key, $criterion)) { $data[$key] = $criterion[$key]; } } if ($doupdate) { $id = $DB->insert_record('gradingform_guide_criteria', $data); } $haschanges[5] = true; } else { // Update criterion in DB. $data = array(); foreach ($criteriafields as $key) { if (array_key_exists($key, $criterion) && $criterion[$key] != $currentcriteria[$id][$key]) { $data[$key] = $criterion[$key]; } } if (!empty($data)) { // Update only if something is changed. $data['id'] = $id; if ($doupdate) { $DB->update_record('gradingform_guide_criteria', $data); } $haschanges[1] = true; } } } // Remove deleted criteria from DB. foreach (array_keys($currentcriteria) as $id) { if (!array_key_exists($id, $newcriteria)) { if ($doupdate) { $DB->delete_records('gradingform_guide_criteria', array('id' => $id)); } $haschanges[3] = true; } } // Now handle comments. if (empty($newdefinition->guide['comments'])) { $newcomment = array(); } else { $newcomment = $newdefinition->guide['comments']; // New ones to be saved. } $currentcomments = $currentdefinition->guide_comments; $commentfields = array('sortorder', 'description'); foreach ($newcomment as $id => $comment) { if (preg_match('/^NEWID\d+$/', $id)) { // Insert criterion into DB. $data = array('definitionid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE); foreach ($commentfields as $key) { if (array_key_exists($key, $comment)) { // Check if key is the comment's description. if ($key === 'description') { // Get a trimmed value for the comment description. $description = trim($comment[$key]); // Check if the comment description is empty. if (empty($description)) { // Continue to the next comment object if the description is empty. continue 2; } } $data[$key] = $comment[$key]; } } if ($doupdate) { $id = $DB->insert_record('gradingform_guide_comments', $data); } } else { // Update criterion in DB. $data = array(); foreach ($commentfields as $key) { if (array_key_exists($key, $comment) && $comment[$key] != $currentcomments[$id][$key]) { $data[$key] = $comment[$key]; } } if (!empty($data)) { // Update only if something is changed. $data['id'] = $id; if ($doupdate) { $DB->update_record('gradingform_guide_comments', $data); } } } } // Remove deleted criteria from DB. foreach (array_keys($currentcomments) as $id) { if (!array_key_exists($id, $newcomment)) { if ($doupdate) { $DB->delete_records('gradingform_guide_comments', array('id' => $id)); } } } // End comments handle. foreach (array('status', 'description', 'descriptionformat', 'name', 'options') as $key) { if (isset($newdefinition->$key) && $newdefinition->$key != $this->definition->$key) { $haschanges[1] = true; } } if ($usermodified && $usermodified != $this->definition->usermodified) { $haschanges[1] = true; } if (!count($haschanges)) { return 0; } if ($doupdate) { parent::update_definition($newdefinition, $usermodified); $this->load_definition(); } // Return the maximum level of changes. $changelevels = array_keys($haschanges); sort($changelevels); return array_pop($changelevels); } /** * Marks all instances filled with this guide with the status INSTANCE_STATUS_NEEDUPDATE */ public function mark_for_regrade() { global $DB; if ($this->has_active_instances()) { $conditions = array('definitionid' => $this->definition->id, 'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE); $DB->set_field('grading_instances', 'status', gradingform_instance::INSTANCE_STATUS_NEEDUPDATE, $conditions); } } /** * Loads the guide form definition if it exists * * There is a new array called 'guide_criteria' appended to the list of parent's definition properties. */ protected function load_definition() { global $DB; // Check to see if the user prefs have changed - putting here as this function is called on post even when // validation on the page fails. - hard to find a better place to locate this as it is specific to the guide. $showdesc = optional_param('showmarkerdesc', null, PARAM_BOOL); // Check if we need to change pref. $showdescstudent = optional_param('showstudentdesc', null, PARAM_BOOL); // Check if we need to change pref. if ($showdesc !== null) { set_user_preference('gradingform_guide-showmarkerdesc', $showdesc); } if ($showdescstudent !== null) { set_user_preference('gradingform_guide-showstudentdesc', $showdescstudent); } // Get definition. $definition = $DB->get_record('grading_definitions', array('areaid' => $this->areaid, 'method' => $this->get_method_name()), '*'); if (!$definition) { // The definition doesn't have to exist. It may be that we are only now creating it. $this->definition = false; return false; } $this->definition = $definition; // Now get criteria. $this->definition->guide_criteria = array(); $this->definition->guide_comments = array(); $criteria = $DB->get_recordset('gradingform_guide_criteria', array('definitionid' => $this->definition->id), 'sortorder'); foreach ($criteria as $criterion) { foreach (array('id', 'sortorder', 'description', 'descriptionformat', 'maxscore', 'descriptionmarkers', 'descriptionmarkersformat', 'shortname') as $fieldname) { if ($fieldname == 'maxscore') { // Strip any trailing 0. $this->definition->guide_criteria[$criterion->id][$fieldname] = (float)$criterion->{$fieldname}; } else { $this->definition->guide_criteria[$criterion->id][$fieldname] = $criterion->{$fieldname}; } } } $criteria->close(); // Now get comments. $comments = $DB->get_recordset('gradingform_guide_comments', array('definitionid' => $this->definition->id), 'sortorder'); foreach ($comments as $comment) { foreach (array('id', 'sortorder', 'description', 'descriptionformat') as $fieldname) { $this->definition->guide_comments[$comment->id][$fieldname] = $comment->{$fieldname}; } } $comments->close(); if (empty($this->moduleinstance)) { // Only set if empty. $modulename = $this->get_component(); $context = $this->get_context(); if (strpos($modulename, 'mod_') === 0) { $dbman = $DB->get_manager(); $modulename = substr($modulename, 4); if ($dbman->table_exists($modulename)) { $cm = get_coursemodule_from_id($modulename, $context->instanceid); if (!empty($cm)) { // This should only occur when the course is being deleted. $this->moduleinstance = $DB->get_record($modulename, array("id"=>$cm->instance)); } } } } } /** * Returns the default options for the guide display * * @return array */ public static function get_default_options() { $options = array( 'alwaysshowdefinition' => 1, 'showmarkspercriterionstudents' => 1, ); return $options; } /** * Gets the options of this guide definition, fills the missing options with default values * * @return array */ public function get_options() { $options = self::get_default_options(); if (!empty($this->definition->options)) { $thisoptions = json_decode($this->definition->options); foreach ($thisoptions as $option => $value) { $options[$option] = $value; } } return $options; } /** * Converts the current definition into an object suitable for the editor form's set_data() * * @param bool $addemptycriterion whether to add an empty criterion if the guide is completely empty (just being created) * @return stdClass */ public function get_definition_for_editing($addemptycriterion = false) { $definition = $this->get_definition(); $properties = new stdClass(); $properties->areaid = $this->areaid; if (isset($this->moduleinstance->grade)) { $properties->modulegrade = $this->moduleinstance->grade; } if ($definition) { foreach (array('id', 'name', 'description', 'descriptionformat', 'status') as $key) { $properties->$key = $definition->$key; } $options = self::description_form_field_options($this->get_context()); $properties = file_prepare_standard_editor($properties, 'description', $options, $this->get_context(), 'grading', 'description', $definition->id); } $properties->guide = array('criteria' => array(), 'options' => $this->get_options(), 'comments' => array()); if (!empty($definition->guide_criteria)) { $properties->guide['criteria'] = $definition->guide_criteria; } else if (!$definition && $addemptycriterion) { $properties->guide['criteria'] = array('addcriterion' => 1); } if (!empty($definition->guide_comments)) { $properties->guide['comments'] = $definition->guide_comments; } else if (!$definition && $addemptycriterion) { $properties->guide['comments'] = array('addcomment' => 1); } return $properties; } /** * Returns the form definition suitable for cloning into another area * * @see parent::get_definition_copy() * @param gradingform_controller $target the controller of the new copy * @return stdClass definition structure to pass to the target's {@link update_definition()} */ public function get_definition_copy(gradingform_controller $target) { $new = parent::get_definition_copy($target); $old = $this->get_definition_for_editing(); $new->description_editor = $old->description_editor; $new->guide = array('criteria' => array(), 'options' => $old->guide['options'], 'comments' => array()); $newcritid = 1; foreach ($old->guide['criteria'] as $oldcritid => $oldcrit) { unset($oldcrit['id']); $new->guide['criteria']['NEWID'.$newcritid] = $oldcrit; $newcritid++; } $newcomid = 1; foreach ($old->guide['comments'] as $oldcritid => $oldcom) { unset($oldcom['id']); $new->guide['comments']['NEWID'.$newcomid] = $oldcom; $newcomid++; } return $new; } /** * Options for displaying the guide description field in the form * * @param context $context * @return array options for the form description field */ public static function description_form_field_options($context) { global $CFG; return array( 'maxfiles' => -1, 'maxbytes' => get_max_upload_file_size($CFG->maxbytes), 'context' => $context, ); } /** * Formats the definition description for display on page * * @return string */ public function get_formatted_description() { if (!isset($this->definition->description)) { return ''; } $context = $this->get_context(); $options = self::description_form_field_options($this->get_context()); $description = file_rewrite_pluginfile_urls($this->definition->description, 'pluginfile.php', $context->id, 'grading', 'description', $this->definition->id, $options); $formatoptions = array( 'noclean' => false, 'trusted' => false, 'filter' => true, 'context' => $context ); return format_text($description, $this->definition->descriptionformat, $formatoptions); } /** * Returns the guide plugin renderer * * @param moodle_page $page the target page * @return gradingform_guide_renderer */ public function get_renderer(moodle_page $page) { return $page->get_renderer('gradingform_'. $this->get_method_name()); } /** * Returns the HTML code displaying the preview of the grading form * * @param moodle_page $page the target page * @return string */ public function render_preview(moodle_page $page) { if (!$this->is_form_defined()) { throw new coding_exception('It is the caller\'s responsibility to make sure that the form is actually defined'); } // Check if current user is able to see preview $options = $this->get_options(); if (empty($options['alwaysshowdefinition']) && !has_capability('moodle/grade:managegradingforms', $page->context)) { return ''; } $criteria = $this->definition->guide_criteria; $comments = $this->definition->guide_comments; $output = $this->get_renderer($page); $guide = ''; $guide .= $output->box($this->get_formatted_description(), 'gradingform_guide-description'); if (has_capability('moodle/grade:managegradingforms', $page->context)) { $guide .= $output->display_guide_mapping_explained($this->get_min_max_score()); $guide .= $output->display_guide($criteria, $comments, $options, self::DISPLAY_PREVIEW, 'guide'); } else { $guide .= $output->display_guide($criteria, $comments, $options, self::DISPLAY_PREVIEW_GRADED, 'guide'); } return $guide; } /** * Deletes the guide definition and all the associated information */ protected function delete_plugin_definition() { global $DB; // Get the list of instances. $instances = array_keys($DB->get_records('grading_instances', array('definitionid' => $this->definition->id), '', 'id')); // Delete all fillings. $DB->delete_records_list('gradingform_guide_fillings', 'instanceid', $instances); // Delete instances. $DB->delete_records_list('grading_instances', 'id', $instances); // Get the list of criteria records. $criteria = array_keys($DB->get_records('gradingform_guide_criteria', array('definitionid' => $this->definition->id), '', 'id')); // Delete critera. $DB->delete_records_list('gradingform_guide_criteria', 'id', $criteria); // Delete comments. $DB->delete_records('gradingform_guide_comments', array('definitionid' => $this->definition->id)); } /** * If instanceid is specified and grading instance exists and it is created by this rater for * this item, this instance is returned. * If there exists a draft for this raterid+itemid, take this draft (this is the change from parent) * Otherwise new instance is created for the specified rater and itemid * * @param int $instanceid * @param int $raterid * @param int $itemid * @return gradingform_instance */ public function get_or_create_instance($instanceid, $raterid, $itemid) { global $DB; if ($instanceid && $instance = $DB->get_record('grading_instances', array('id' => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) { return $this->get_instance($instance); } if ($itemid && $raterid) { $params = array('definitionid' => $this->definition->id, 'raterid' => $raterid, 'itemid' => $itemid); if ($rs = $DB->get_records('grading_instances', $params, 'timemodified DESC', '*', 0, 1)) { $record = reset($rs); $currentinstance = $this->get_current_instance($raterid, $itemid); if ($record->status == gradingform_guide_instance::INSTANCE_STATUS_INCOMPLETE && (!$currentinstance || $record->timemodified > $currentinstance->get_data('timemodified'))) { $record->isrestored = true; return $this->get_instance($record); } } } return $this->create_instance($raterid, $itemid); } /** * Returns html code to be included in student's feedback. * * @param moodle_page $page * @param int $itemid * @param array $gradinginfo result of function grade_get_grades * @param string $defaultcontent default string to be returned if no active grading is found * @param bool $cangrade whether current user has capability to grade in this context * @return string */ public function render_grade($page, $itemid, $gradinginfo, $defaultcontent, $cangrade) { return $this->get_renderer($page)->display_instances($this->get_active_instances($itemid), $defaultcontent, $cangrade); } // Full-text search support. /** * Prepare the part of the search query to append to the FROM statement * * @param string $gdid the alias of grading_definitions.id column used by the caller * @return string */ public static function sql_search_from_tables($gdid) { return " LEFT JOIN {gradingform_guide_criteria} gc ON (gc.definitionid = $gdid)"; } /** * Prepare the parts of the SQL WHERE statement to search for the given token * * The returned array cosists of the list of SQL comparions and the list of * respective parameters for the comparisons. The returned chunks will be joined * with other conditions using the OR operator. * * @param string $token token to search for * @return array An array containing two more arrays * Array of search SQL fragments * Array of params for the search fragments */ public static function sql_search_where($token) { global $DB; $subsql = array(); $params = array(); // Search in guide criteria description. $subsql[] = $DB->sql_like('gc.description', '?', false, false); $params[] = '%'.$DB->sql_like_escape($token).'%'; return array($subsql, $params); } /** * Calculates and returns the possible minimum and maximum score (in points) for this guide * * @return array */ public function get_min_max_score() { if (!$this->is_form_available()) { return null; } $returnvalue = array('minscore' => 0, 'maxscore' => 0); $maxscore = 0; foreach ($this->get_definition()->guide_criteria as $id => $criterion) { $maxscore += $criterion['maxscore']; } $returnvalue['maxscore'] = $maxscore; $returnvalue['minscore'] = 0; if (!$this->is_shared_template()) { $fieldname = \core_grades\component_gradeitems::get_field_name_for_itemname($this->component, $this->area, 'grade'); if (!empty($this->moduleinstance->{$fieldname})) { $graderange = make_grades_menu($this->moduleinstance->{$fieldname}); $returnvalue['modulegrade'] = count($graderange) - 1; } } return $returnvalue; } /** * @return array An array containing 2 key/value pairs which hold the external_multiple_structure * for the 'guide_criteria' and the 'guide_comments'. * @see gradingform_controller::get_external_definition_details() * @since Moodle 2.5 */ public static function get_external_definition_details() { $guide_criteria = new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'criterion id', VALUE_OPTIONAL), 'sortorder' => new external_value(PARAM_INT, 'sortorder', VALUE_OPTIONAL), 'description' => new external_value(PARAM_RAW, 'description', VALUE_OPTIONAL), 'descriptionformat' => new external_format_value('description', VALUE_OPTIONAL), 'shortname' => new external_value(PARAM_TEXT, 'description'), 'descriptionmarkers' => new external_value(PARAM_RAW, 'markers description', VALUE_OPTIONAL), 'descriptionmarkersformat' => new external_format_value('descriptionmarkers', VALUE_OPTIONAL), 'maxscore' => new external_value(PARAM_FLOAT, 'maximum score') ) ) ); $guide_comments = new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'criterion id', VALUE_OPTIONAL), 'sortorder' => new external_value(PARAM_INT, 'sortorder', VALUE_OPTIONAL), 'description' => new external_value(PARAM_RAW, 'description', VALUE_OPTIONAL), 'descriptionformat' => new external_format_value('description', VALUE_OPTIONAL) ) ), 'comments', VALUE_OPTIONAL ); return array('guide_criteria' => $guide_criteria, 'guide_comments' => $guide_comments); } /** * Returns an array that defines the structure of the guide's filling. This function is used by * the web service function core_grading_external::get_gradingform_instances(). * * @return An array containing a single key/value pair with the 'criteria' external_multiple_structure * @see gradingform_controller::get_external_instance_filling_details() * @since Moodle 2.6 */ public static function get_external_instance_filling_details() { $criteria = new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'filling id'), 'criterionid' => new external_value(PARAM_INT, 'criterion id'), 'levelid' => new external_value(PARAM_INT, 'level id', VALUE_OPTIONAL), 'remark' => new external_value(PARAM_RAW, 'remark', VALUE_OPTIONAL), 'remarkformat' => new external_format_value('remark', VALUE_OPTIONAL), 'score' => new external_value(PARAM_FLOAT, 'maximum score') ) ), 'filling', VALUE_OPTIONAL ); return array ('criteria' => $criteria); } } /** * Class to manage one guide grading instance. Stores information and performs actions like * update, copy, validate, submit, etc. * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradingform_guide_instance extends gradingform_instance { /** @var array */ protected $guide; /** @var array An array of validation errors */ protected $validationerrors = array(); /** * Deletes this (INCOMPLETE) instance from database. */ public function cancel() { global $DB; parent::cancel(); $DB->delete_records('gradingform_guide_fillings', array('instanceid' => $this->get_id())); } /** * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with * the specified values) * * @param int $raterid value for raterid in the duplicate * @param int $itemid value for itemid in the duplicate * @return int id of the new instance */ public function copy($raterid, $itemid) { global $DB; $instanceid = parent::copy($raterid, $itemid); $currentgrade = $this->get_guide_filling(); foreach ($currentgrade['criteria'] as $criterionid => $record) { $params = array('instanceid' => $instanceid, 'criterionid' => $criterionid, 'score' => $record['score'], 'remark' => $record['remark'], 'remarkformat' => $record['remarkformat']); $DB->insert_record('gradingform_guide_fillings', $params); } return $instanceid; } /** * Determines whether the submitted form was empty. * * @param array $elementvalue value of element submitted from the form * @return boolean true if the form is empty */ public function is_empty_form($elementvalue) { $criteria = $this->get_controller()->get_definition()->guide_criteria; foreach ($criteria as $id => $criterion) { $score = $elementvalue['criteria'][$id]['score']; $remark = $elementvalue['criteria'][$id]['remark']; if ((isset($score) && $score !== '') || ((isset($remark) && $remark !== ''))) { return false; } } return true; } /** * Validates that guide is fully completed and contains valid grade on each criterion * * @param array $elementvalue value of element as came in form submit * @return boolean true if the form data is validated and contains no errors */ public function validate_grading_element($elementvalue) { $criteria = $this->get_controller()->get_definition()->guide_criteria; if (!isset($elementvalue['criteria']) || !is_array($elementvalue['criteria']) || count($elementvalue['criteria']) < count($criteria)) { return false; } // Reset validation errors. $this->validationerrors = null; foreach ($criteria as $id => $criterion) { if (!isset($elementvalue['criteria'][$id]['score']) || $criterion['maxscore'] < $elementvalue['criteria'][$id]['score'] || !is_numeric($elementvalue['criteria'][$id]['score']) || $elementvalue['criteria'][$id]['score'] < 0) { $this->validationerrors[$id]['score'] = $elementvalue['criteria'][$id]['score']; } } if (!empty($this->validationerrors)) { return false; } return true; } /** * Retrieves from DB and returns the data how this guide was filled * * @param bool $force whether to force DB query even if the data is cached * @return array */ public function get_guide_filling($force = false) { global $DB; if ($this->guide === null || $force) { $records = $DB->get_records('gradingform_guide_fillings', array('instanceid' => $this->get_id())); $this->guide = array('criteria' => array()); foreach ($records as $record) { $record->score = (float)$record->score; // Strip trailing 0. $this->guide['criteria'][$record->criterionid] = (array)$record; } } return $this->guide; } /** * Updates the instance with the data received from grading form. This function may be * called via AJAX when grading is not yet completed, so it does not change the * status of the instance. * * @param array $data */ public function update($data) { global $DB; $currentgrade = $this->get_guide_filling(); parent::update($data); foreach ($data['criteria'] as $criterionid => $record) { // Hardcoding/defaulting to html format for new/existing record $record['remarkformat'] = FORMAT_HTML; if (!array_key_exists($criterionid, $currentgrade['criteria'])) { $newrecord = array('instanceid' => $this->get_id(), 'criterionid' => $criterionid, 'score' => $record['score'], 'remarkformat' => $record['remarkformat']); if (isset($record['remark'])) { $newrecord['remark'] = $record['remark']; } $DB->insert_record('gradingform_guide_fillings', $newrecord); } else { $newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']); foreach (array('score', 'remark', 'remarkformat') as $key) { if (isset($record[$key]) && $currentgrade['criteria'][$criterionid][$key] != $record[$key]) { $newrecord[$key] = $record[$key]; } } if (count($newrecord) > 1) { $DB->update_record('gradingform_guide_fillings', $newrecord); } } } foreach ($currentgrade['criteria'] as $criterionid => $record) { if (!array_key_exists($criterionid, $data['criteria'])) { $DB->delete_records('gradingform_guide_fillings', array('id' => $record['id'])); } } $this->get_guide_filling(true); } /** * Removes the attempt from the gradingform_guide_fillings table * @param array $data the attempt data */ public function clear_attempt($data) { global $DB; foreach ($data['criteria'] as $criterionid => $record) { $DB->delete_records('gradingform_guide_fillings', array('criterionid' => $criterionid, 'instanceid' => $this->get_id())); } } /** * Calculates the grade to be pushed to the gradebook * * @return float|int the valid grade from $this->get_controller()->get_grade_range() */ public function get_grade() { $grade = $this->get_guide_filling(); if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) { return -1; } $graderange = array_keys($this->get_controller()->get_grade_range()); if (empty($graderange)) { return -1; } sort($graderange); $mingrade = $graderange[0]; $maxgrade = $graderange[count($graderange) - 1]; $curscore = 0; foreach ($grade['criteria'] as $record) { $curscore += $record['score']; } $gradeoffset = ($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])* ($maxgrade-$mingrade); if ($this->get_controller()->get_allow_grade_decimals()) { return $gradeoffset + $mingrade; } return round($gradeoffset, 0) + $mingrade; } /** * Returns html for form element of type 'grading'. * * @param moodle_page $page * @param MoodleQuickForm_grading $gradingformelement * @return string */ public function render_grading_element($page, $gradingformelement) { if (!$gradingformelement->_flagFrozen) { $module = array('name'=>'gradingform_guide', 'fullpath'=>'/grade/grading/form/guide/js/guide.js'); $page->requires->js_init_call('M.gradingform_guide.init', array( array('name' => $gradingformelement->getName())), true, $module); $mode = gradingform_guide_controller::DISPLAY_EVAL; } else { if ($gradingformelement->_persistantFreeze) { $mode = gradingform_guide_controller::DISPLAY_EVAL_FROZEN; } else { $mode = gradingform_guide_controller::DISPLAY_REVIEW; } } $criteria = $this->get_controller()->get_definition()->guide_criteria; $comments = $this->get_controller()->get_definition()->guide_comments; $options = $this->get_controller()->get_options(); $value = $gradingformelement->getValue(); $html = ''; if ($value === null) { $value = $this->get_guide_filling(); } else if (!$this->validate_grading_element($value)) { $html .= html_writer::tag('div', get_string('guidenotcompleted', 'gradingform_guide'), array('class' => 'gradingform_guide-error')); if (!empty($this->validationerrors)) { foreach ($this->validationerrors as $id => $err) { $a = new stdClass(); $a->criterianame = format_text($criteria[$id]['shortname'], FORMAT_HTML); $a->maxscore = $criteria[$id]['maxscore']; if ($this->validationerrors[$id]['score'] < 0) { $html .= html_writer::tag('div', get_string('err_scoreisnegative', 'gradingform_guide', $a), array('class' => 'gradingform_guide-error')); } else { $html .= html_writer::tag('div', get_string('err_scoreinvalid', 'gradingform_guide', $a), array('class' => 'gradingform_guide-error')); } } } } $currentinstance = $this->get_current_instance(); if ($currentinstance && $currentinstance->get_status() == gradingform_instance::INSTANCE_STATUS_NEEDUPDATE) { $html .= html_writer::tag('div', get_string('needregrademessage', 'gradingform_guide'), array('class' => 'gradingform_guide-regrade', 'role' => 'alert')); } $haschanges = false; if ($currentinstance) { $curfilling = $currentinstance->get_guide_filling(); foreach ($curfilling['criteria'] as $criterionid => $curvalues) { $value['criteria'][$criterionid]['score'] = $curvalues['score']; $newremark = null; $newscore = null; if (isset($value['criteria'][$criterionid]['remark'])) { $newremark = $value['criteria'][$criterionid]['remark']; } if (isset($value['criteria'][$criterionid]['score'])) { $newscore = $value['criteria'][$criterionid]['score']; } if ($newscore != $curvalues['score'] || $newremark != $curvalues['remark']) { $haschanges = true; } } } if ($this->get_data('isrestored') && $haschanges) { $html .= html_writer::tag('div', get_string('restoredfromdraft', 'gradingform_guide'), array('class' => 'gradingform_guide-restored')); } $html .= html_writer::tag('div', $this->get_controller()->get_formatted_description(), array('class' => 'gradingform_guide-description')); $html .= $this->get_controller()->get_renderer($page)->display_guide($criteria, $comments, $options, $mode, $gradingformelement->getName(), $value, $this->validationerrors); return $html; } } /** * Get the icon mapping for font-awesome. * * @return array */ function gradingform_guide_get_fontawesome_icon_map(): array { return [ 'gradingform_guide:info' => 'fa-circle-info', 'gradingform_guide:plus' => 'fa-circle-plus', ]; } PK'\X}@@form/guide/version.phpnu[. /** * Marking guide, advanced grade plugin * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'gradingform_guide'; $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->maturity = MATURITY_STABLE; PK'\˫22@form/guide/classes/grades/grader/gradingpanel/external/fetch.phpnu[. /** * Web services relating to fetching of a marking guide for the grading panel. * * @package gradingform_guide * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types = 1); namespace gradingform_guide\grades\grader\gradingpanel\external; global $CFG; use coding_exception; use context; use core_user; use core_grades\component_gradeitem as gradeitem; use core_grades\component_gradeitems; use core_external\external_api; 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 moodle_exception; use stdClass; require_once($CFG->dirroot.'/grade/grading/form/guide/lib.php'); /** * Web services relating to fetching of a marking guide for the grading panel. * * @package gradingform_guide * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class fetch extends external_api { /** * Describes the parameters for fetching the grading panel for a simple grade. * * @return external_function_parameters * @since Moodle 3.8 */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters ([ 'component' => new external_value( PARAM_ALPHANUMEXT, 'The name of the component', VALUE_REQUIRED ), 'contextid' => new external_value( PARAM_INT, 'The ID of the context being graded', VALUE_REQUIRED ), 'itemname' => new external_value( PARAM_ALPHANUM, 'The grade item itemname being graded', VALUE_REQUIRED ), 'gradeduserid' => new external_value( PARAM_INT, 'The ID of the user show', VALUE_REQUIRED ), ]); } /** * Fetch the data required to build a grading panel for a simple grade. * * @param string $component * @param int $contextid * @param string $itemname * @param int $gradeduserid * @return array * @throws \dml_exception * @throws \invalid_parameter_exception * @throws \restricted_context_exception * @throws coding_exception * @throws moodle_exception * @since Moodle 3.8 */ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid): array { global $CFG, $USER; require_once("{$CFG->libdir}/gradelib.php"); [ 'component' => $component, 'contextid' => $contextid, 'itemname' => $itemname, 'gradeduserid' => $gradeduserid, ] = self::validate_parameters(self::execute_parameters(), [ 'component' => $component, 'contextid' => $contextid, 'itemname' => $itemname, 'gradeduserid' => $gradeduserid, ]); // Validate the context. $context = context::instance_by_id($contextid); self::validate_context($context); // Validate that the supplied itemname is a gradable item. if (!component_gradeitems::is_valid_itemname($component, $itemname)) { throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component"); } // Fetch the gradeitem instance. $gradeitem = gradeitem::instance($component, $context, $itemname); if (MARKING_GUIDE !== $gradeitem->get_advanced_grading_method()) { throw new moodle_exception( "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a marking guide" ); } // Fetch the actual data. $gradeduser = core_user::get_user($gradeduserid, '*', MUST_EXIST); // One can access its own grades. Others just if they're graders. if ($gradeduserid != $USER->id) { $gradeitem->require_user_can_grade($gradeduser, $USER); } return self::get_fetch_data($gradeitem, $gradeduser); } /** * Get the data to be fetched. * * @param gradeitem $gradeitem * @param stdClass $gradeduser * @return array */ public static function get_fetch_data(gradeitem $gradeitem, stdClass $gradeduser): array { global $USER; $hasgrade = $gradeitem->user_has_grade($gradeduser); $grade = $gradeitem->get_formatted_grade_for_user($gradeduser, $USER); $instance = $gradeitem->get_advanced_grading_instance($USER, $grade); if (!$instance) { throw new moodle_exception('error:gradingunavailable', 'grading'); } $controller = $instance->get_controller(); $definition = $controller->get_definition(); $fillings = $instance->get_guide_filling(); $context = $controller->get_context(); $definitionid = (int) $definition->id; // Set up some items we need to return on other interfaces. $gradegrade = \grade_grade::fetch(['itemid' => $gradeitem->get_grade_item()->id, 'userid' => $gradeduser->id]); $gradername = $gradegrade ? fullname(\core_user::get_user($gradegrade->usermodified)) : null; $maxgrade = max(array_keys($controller->get_grade_range())); $criterion = []; if ($definition->guide_criteria) { $criterion = array_map(function($criterion) use ($definitionid, $fillings, $context) { $result = [ 'id' => $criterion['id'], 'name' => $criterion['shortname'], 'maxscore' => $criterion['maxscore'], 'description' => self::get_formatted_text( $context, $definitionid, 'description', $criterion['description'], (int) $criterion['descriptionformat'] ), 'descriptionmarkers' => self::get_formatted_text( $context, $definitionid, 'descriptionmarkers', $criterion['descriptionmarkers'], (int) $criterion['descriptionmarkersformat'] ), 'score' => null, 'remark' => null, ]; if (array_key_exists($criterion['id'], $fillings['criteria'])) { $filling = $fillings['criteria'][$criterion['id']]; $result['score'] = $filling['score']; $result['remark'] = self::get_formatted_text( $context, $definitionid, 'remark', $filling['remark'], (int) FORMAT_HTML ); } return $result; }, $definition->guide_criteria); } $comments = []; if ($definition->guide_comments) { $comments = array_map(function($comment) use ($definitionid, $context) { return [ 'id' => $comment['id'], 'sortorder' => $comment['sortorder'], 'description' => self::get_formatted_text( $context, $definitionid, 'description', $comment['description'], (int) $comment['descriptionformat'] ), ]; }, $definition->guide_comments); } return [ 'templatename' => 'gradingform_guide/grades/grader/gradingpanel', 'hasgrade' => $hasgrade, 'grade' => [ 'instanceid' => $instance->get_id(), 'criterion' => $criterion, 'hascomments' => !empty($comments), 'comments' => $comments, 'usergrade' => $grade->usergrade, 'maxgrade' => $maxgrade, 'gradedby' => $gradername, 'timecreated' => $grade->timecreated, 'timemodified' => $grade->timemodified, ], 'warnings' => [], ]; } /** * Describes the data returned from the external function. * * @return external_single_structure * @since Moodle 3.8 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'templatename' => new external_value(PARAM_SAFEPATH, 'The template to use when rendering this data'), 'hasgrade' => new external_value(PARAM_BOOL, 'Does the user have a grade?'), 'grade' => new external_single_structure([ 'instanceid' => new external_value(PARAM_INT, 'The id of the current grading instance'), 'criterion' => new external_multiple_structure( new external_single_structure([ 'id' => new external_value(PARAM_INT, 'The id of the criterion'), 'name' => new external_value(PARAM_RAW, 'The name of the criterion'), 'maxscore' => new external_value(PARAM_FLOAT, 'The maximum score for this criterion'), 'description' => new external_value(PARAM_RAW, 'The description of the criterion'), 'descriptionmarkers' => new external_value(PARAM_RAW, 'The description of the criterion for markers'), 'score' => new external_value(PARAM_FLOAT, 'The current score for user being assessed', VALUE_OPTIONAL), 'remark' => new external_value(PARAM_RAW, 'Any remarks for this criterion for the user being assessed', VALUE_OPTIONAL), ]), 'The criterion by which this item will be graded' ), 'hascomments' => new external_value(PARAM_BOOL, 'Whether there are any frequently-used comments'), 'comments' => new external_multiple_structure( new external_single_structure([ 'id' => new external_value(PARAM_INT, 'Comment id'), 'sortorder' => new external_value(PARAM_INT, 'The sortorder of this comment'), 'description' => new external_value(PARAM_RAW, 'The comment value'), ]), 'Frequently used comments' ), 'usergrade' => new external_value(PARAM_RAW, 'Current user grade'), 'maxgrade' => new external_value(PARAM_RAW, 'Max possible grade'), 'gradedby' => new external_value(PARAM_RAW, 'The assumed grader of this grading instance'), 'timecreated' => new external_value(PARAM_INT, 'The time that the grade was created'), 'timemodified' => new external_value(PARAM_INT, 'The time that the grade was last updated'), ]), 'warnings' => new external_warnings(), ]); } /** * Get a formatted version of the remark/description/etc. * * @param context $context * @param int $definitionid * @param string $filearea The file area of the field * @param string $text The text to be formatted * @param int $format The input format of the string * @return string */ protected static function get_formatted_text(context $context, int $definitionid, string $filearea, string $text, int $format): string { $formatoptions = [ 'noclean' => false, 'trusted' => false, 'filter' => true, ]; [$newtext] = \core_external\util::format_text( $text, $format, $context, 'grading', $filearea, $definitionid, $formatoptions ); return $newtext; } } PK'\j@form/guide/classes/grades/grader/gradingpanel/external/store.phpnu[. /** * Web services relating to fetching of a marking guide for the grading panel. * * @package gradingform_guide * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types = 1); namespace gradingform_guide\grades\grader\gradingpanel\external; global $CFG; use coding_exception; use context; use core_grades\component_gradeitem as gradeitem; use core_grades\component_gradeitems; use core_user; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use moodle_exception; require_once($CFG->dirroot.'/grade/grading/form/guide/lib.php'); /** * Web services relating to storing of a marking guide for the grading panel. * * @package gradingform_guide * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class store extends external_api { /** * Describes the parameters for storing the grading panel for a simple grade. * * @return external_function_parameters * @since Moodle 3.8 */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters ([ 'component' => new external_value( PARAM_ALPHANUMEXT, 'The name of the component', VALUE_REQUIRED ), 'contextid' => new external_value( PARAM_INT, 'The ID of the context being graded', VALUE_REQUIRED ), 'itemname' => new external_value( PARAM_ALPHANUM, 'The grade item itemname being graded', VALUE_REQUIRED ), 'gradeduserid' => new external_value( PARAM_INT, 'The ID of the user show', VALUE_REQUIRED ), 'notifyuser' => new external_value( PARAM_BOOL, 'Wheteher to notify the user or not', VALUE_DEFAULT, false ), 'formdata' => new external_value( PARAM_RAW, 'The serialised form data representing the grade', VALUE_REQUIRED ), ]); } /** * Fetch the data required to build a grading panel for a simple grade. * * @param string $component * @param int $contextid * @param string $itemname * @param int $gradeduserid * @param bool $notifyuser * @param string $formdata * * @return array * @throws \dml_exception * @throws \invalid_parameter_exception * @throws \restricted_context_exception * @throws coding_exception * @throws moodle_exception * @since Moodle 3.8 */ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid, bool $notifyuser, string $formdata): array { global $USER; [ 'component' => $component, 'contextid' => $contextid, 'itemname' => $itemname, 'gradeduserid' => $gradeduserid, 'notifyuser' => $notifyuser, 'formdata' => $formdata, ] = self::validate_parameters(self::execute_parameters(), [ 'component' => $component, 'contextid' => $contextid, 'itemname' => $itemname, 'gradeduserid' => $gradeduserid, 'notifyuser' => $notifyuser, 'formdata' => $formdata, ]); // Validate the context. $context = context::instance_by_id($contextid); self::validate_context($context); // Validate that the supplied itemname is a gradable item. if (!component_gradeitems::is_valid_itemname($component, $itemname)) { throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component"); } // Fetch the gradeitem instance. $gradeitem = gradeitem::instance($component, $context, $itemname); // Validate that this gradeitem is actually enabled. if (!$gradeitem->is_grading_enabled()) { throw new moodle_exception("Grading is not enabled for {$itemname} in this context"); } // Fetch the record for the graded user. $gradeduser = core_user::get_user($gradeduserid); // Require that this user can save grades. $gradeitem->require_user_can_grade($gradeduser, $USER); if (MARKING_GUIDE !== $gradeitem->get_advanced_grading_method()) { throw new moodle_exception( "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a marking guide" ); } // Parse the serialised string into an object. $data = []; parse_str($formdata, $data); // Grade. $gradeitem->store_grade_from_formdata($gradeduser, $USER, (object) $data); // Notify. if ($notifyuser) { // Send notification. $gradeitem->send_student_notification($gradeduser, $USER); } return fetch::get_fetch_data($gradeitem, $gradeduser); } /** * Describes the data returned from the external function. * * @return external_single_structure * @since Moodle 3.8 */ public static function execute_returns(): external_single_structure { return fetch::execute_returns(); } } PK'\9qq9  'form/guide/classes/privacy/provider.phpnu[. /** * Privacy class for requesting user data. * * @package gradingform_guide * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace gradingform_guide\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\transform; use \core_privacy\local\request\writer; /** * Privacy class for requesting user data. * * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_grading\privacy\gradingform_provider_v2, \core_privacy\local\request\user_preference_provider { /** * Return the fields which contain personal data. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_database_table('gradingform_guide_fillings', [ 'instanceid' => 'privacy:metadata:instanceid', 'criterionid' => 'privacy:metadata:criterionid', 'remark' => 'privacy:metadata:remark', 'score' => 'privacy:metadata:score' ], 'privacy:metadata:fillingssummary'); $collection->add_user_preference( 'gradingform_guide-showmarkerdesc', 'privacy:metadata:preference:showmarkerdesc' ); $collection->add_user_preference( 'gradingform_guide-showstudentdesc', 'privacy:metadata:preference:showstudentdesc' ); return $collection; } /** * Export user data relating to an instance ID. * * @param \context $context Context to use with the export writer. * @param int $instanceid The instance ID to export data for. * @param array $subcontext The directory to export this data to. */ public static function export_gradingform_instance_data(\context $context, int $instanceid, array $subcontext) { global $DB; // Get records from the provided params. $params = ['instanceid' => $instanceid]; $sql = "SELECT gc.shortname, gc.description, gc.maxscore, gf.score, gf.remark FROM {gradingform_guide_fillings} gf JOIN {gradingform_guide_criteria} gc ON gc.id = gf.criterionid WHERE gf.instanceid = :instanceid"; $records = $DB->get_records_sql($sql, $params); if ($records) { $subcontext = array_merge($subcontext, [get_string('guide', 'gradingform_guide'), $instanceid]); writer::with_context($context)->export_data($subcontext, (object) $records); } } /** * Deletes all user data related to the provided instance IDs. * * @param array $instanceids The instance IDs to delete information from. */ public static function delete_gradingform_for_instances(array $instanceids) { global $DB; $DB->delete_records_list('gradingform_guide_fillings', 'instanceid', $instanceids); } /** * Store all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { $prefvalue = get_user_preferences('gradingform_guide-showmarkerdesc', null, $userid); if ($prefvalue !== null) { $transformedvalue = transform::yesno($prefvalue); writer::export_user_preference( 'gradingform_guide', 'gradingform_guide-showmarkerdesc', $transformedvalue, get_string('privacy:metadata:preference:showmarkerdesc', 'gradingform_guide') ); } $prefvalue = get_user_preferences('gradingform_guide-showstudentdesc', null, $userid); if ($prefvalue !== null) { $transformedvalue = transform::yesno($prefvalue); writer::export_user_preference( 'gradingform_guide', 'gradingform_guide-showstudentdesc', $transformedvalue, get_string('privacy:metadata:preference:showstudentdesc', 'gradingform_guide') ); } } } PK'\c8form/guide/templates/grades/grader/gradingpanel.mustachenu[{{! 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 gradingform_guide/grades/grader/gradingpanel Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * instanceid: Instance of the module this grading form belongs too * criterion: A gradeable item in the Marking Guide * name: Name of the gradeable item * id: ID of the gradeable item * description: Description shown to students for this gradeable item * descriptionmarkers: Description shown to teachers for this gradeable item * maxscore: Max allowable assinable points for this item * score: Current score assigned to the learner for this item * remark: Text input for the teacher to relay to the student * hascomments: Flag for frequently used comments * comments: Array of frequently used comments * description: Description of a frequently used comment Example context (json): { "instanceid": "42", "criterion": [ { "name": "Motivation", "id": 13, "description": "Show your motivation to rock climbing", "descriptionmarkers": "Does the student show interest in climbing?", "maxscore": 37, "score": 20, "remark": "That's great!", "hascomments": true, "comments": [ {"description": "Great work!"}, {"description": "You should really try it before jumping to conclusions"} ] } ] } }}
{{#criterion}}
{{name}}
{{{description}}} {{#descriptionmarkers}}
{{{descriptionmarkers}}} {{/descriptionmarkers}}
{{#hascomments}} {{/hascomments}}
{{#hascomments}}
{{#str}}comments, gradingform_guide{{/str}}
{{#comments}} {{/comments}}
{{/hascomments}}
{{/criterion}}
{{#js}} require(['gradingform_guide/grades/grader/gradingpanel/comments', 'core/auto_rows'], function(Comments, AutoRows) { Comments.init('gradingform_guide-{{uniqid}}'); AutoRows.init(document.getElementById('gradingform_guide-{{uniqid}}')); }); {{/js}} PK'\~TT-form/guide/templates/comment_chooser.mustachenu[{{! 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 gradingform_guide/comment_chooser Moodle comment chooser template for marking guide. The purpose of this template is to render a list of frequently used comments that can be used by the comment chooser dialog. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * criterionId The criterion ID this chooser template is being generated for. * comments Array of id / description pairs. Example context (json): { "criterionId": "1", "comments": [ { "id": "1", "description": "Test comment description 1" }, { "id": "2", "description": "Test comment description 2" } ] } }}
{{#comments}} {{/comments}}
PK'\form/guide/preview.phpnu[. /** * Preview marking guide page * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__.'/../../../../config.php'); require_once(__DIR__.'/lib.php'); require_once(__DIR__.'/edit_form.php'); require_once($CFG->dirroot.'/grade/grading/lib.php'); $areaid = required_param('areaid', PARAM_INT); $manager = get_grading_manager($areaid); list($context, $course, $cm) = get_context_info_array($manager->get_context()->id); require_login($course, true, $cm); $controller = $manager->get_controller('guide'); $options = $controller->get_options(); if (!$controller->is_form_defined() || empty($options['alwaysshowdefinition'])) { throw new moodle_exception('nopermissions', 'error', '', get_string('previewmarkingguide', 'gradingform_guide')); } $title = get_string('gradingof', 'gradingform_guide', $manager->get_area_title()); $PAGE->set_url(new moodle_url('/grade/grading/form/guide/preview.php', array('areaid' => $areaid))); $PAGE->set_title($title); $PAGE->set_heading($title); echo $OUTPUT->header(); echo $OUTPUT->heading($title); echo $controller->render_preview($PAGE); echo $OUTPUT->footer(); PK'\weG G form/guide/edit.phpnu[. /** * Rubric editor page * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__.'/../../../../config.php'); require_once(__DIR__.'/lib.php'); require_once(__DIR__.'/edit_form.php'); require_once($CFG->dirroot.'/grade/grading/lib.php'); $areaid = required_param('areaid', PARAM_INT); $manager = get_grading_manager($areaid); list($context, $course, $cm) = get_context_info_array($manager->get_context()->id); require_login($course, true, $cm); require_capability('moodle/grade:managegradingforms', $context); $controller = $manager->get_controller('guide'); $PAGE->set_url(new moodle_url('/grade/grading/form/guide/edit.php', array('areaid' => $areaid))); $PAGE->set_title(get_string('definemarkingguide', 'gradingform_guide')); $PAGE->set_heading(get_string('definemarkingguide', 'gradingform_guide')); $mform = new gradingform_guide_editguide(null, array('areaid' => $areaid, 'context' => $context, 'allowdraft' => !$controller->has_active_instances()), 'post', '', array('class' => 'gradingform_guide_editform')); $data = $controller->get_definition_for_editing(true); $returnurl = optional_param('returnurl', $manager->get_management_url(), PARAM_LOCALURL); $data->returnurl = $returnurl; $mform->set_data($data); if ($mform->is_cancelled()) { redirect($returnurl); } else if ($mform->is_submitted() && $mform->is_validated() && !$mform->need_confirm_regrading($controller)) { // Everything ok, validated, re-grading confirmed if needed. Make changes to the rubric. $controller->update_definition($mform->get_data()); redirect($returnurl); } // Try to keep the session alive on this page as it may take some time // before significant interaction happens with the server. \core\session\manager::keepalive(); echo $OUTPUT->header(); $mform->display(); echo $OUTPUT->footer(); PK'\[form/guide/READMEnu[Marking Guide grading form written by Dan Marsden based on Lightwork Rubric type 2 format and the spec available here: http://docs.moodle.org/dev/Lightwork PK'\b 0form/guide/amd/src/grades/grader/gradingpanel.jsnu[// 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 . /** * Grading panel for gradingform_guide. * * @module gradingform_guide/grades/grader/gradingpanel * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {call as fetchMany} from 'core/ajax'; import {normaliseResult} from 'core_grades/grades/grader/gradingpanel/normalise'; import {compareData} from 'core_grades/grades/grader/gradingpanel/comparison'; // Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send() import jQuery from 'jquery'; /** * For a given component, contextid, itemname & gradeduserid we can fetch the currently assigned grade. * * @param {String} component * @param {Number} contextid * @param {String} itemname * @param {Number} gradeduserid * * @returns {Promise} */ export const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => { return fetchMany([{ methodname: `gradingform_guide_grader_gradingpanel_fetch`, args: { component, contextid, itemname, gradeduserid, }, }])[0]; }; /** * For a given component, contextid, itemname & gradeduserid we can store the currently assigned grade in a given form. * * @param {String} component * @param {Number} contextid * @param {String} itemname * @param {Number} gradeduserid * @param {Boolean} notifyUser * @param {HTMLElement} rootNode * * @returns {Promise} */ export const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, notifyUser, rootNode) => { const form = rootNode.querySelector('form'); if (compareData(form) === true) { return normaliseResult(await fetchMany([{ methodname: `gradingform_guide_grader_gradingpanel_store`, args: { component, contextid, itemname, gradeduserid, notifyuser: notifyUser, formdata: jQuery(form).serialize(), }, }])[0]); } else { return ''; } }; PK'\cOO9form/guide/amd/src/grades/grader/gradingpanel/comments.jsnu[// 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 . /** * Grading panel frequently used comments selector. * * @module gradingform_guide/grades/grader/gradingpanel/comments * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Selectors from './comments/selectors'; /** * Manage the frequently used comments in the Marking Guide form. * * @param {String} rootId */ export const init = (rootId) => { const rootNode = document.querySelector(`#${rootId}`); rootNode.addEventListener('click', (e) => { if (!e.target.matches(Selectors.frequentComment)) { return; } e.preventDefault(); const clicked = e.target.closest(Selectors.frequentComment); const criterion = clicked.closest(Selectors.criterion); const remark = criterion.querySelector(Selectors.remark); if (!remark) { return; } // Either append the comment to an existing comment or set it as the comment. if (remark.value.trim()) { remark.value += `\n${clicked.innerHTML}`; } else { remark.value += clicked.innerHTML; } }); }; PK'\ pCform/guide/amd/src/grades/grader/gradingpanel/comments/selectors.jsnu[// 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 . /** * Define all of the selectors we will be using on the Marking Guide interface. * * @module gradingform_guide/grades/grader/gradingpanel/comments/selectors * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default { frequentComment: '[data-gradingform_guide-role="frequent-comment"]', criterion: '[data-gradingform-guide-role="criterion"]', remark: '[data-gradingform-guide-role="remark"]', }; PK'\ y5GG%form/guide/amd/src/comment_chooser.jsnu[// 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 . /** * AMD code for the frequently used comments chooser for the marking guide grading form. * * @module gradingform_guide/comment_chooser * @copyright 2015 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/templates', 'core/key_codes', 'core/notification', 'core/yui'], function($, templates, keycodes, notification) { // Private variables and functions. return /** @alias module:gradingform_guide/comment_chooser */ { // Public variables and functions. /** * Initialises the module. * * Basically, it performs the binding and handling of the button click event for * the 'Insert frequently used comment' button. * * @param {Integer} criterionId The criterion ID. * @param {String} buttonId The element ID of the button which the handler will be bound to. * @param {String} remarkId The element ID of the remark text area where the text of the selected comment will be copied to. * @param {Array} commentOptions The array of frequently used comments to be used as options. */ initialise: function(criterionId, buttonId, remarkId, commentOptions) { /** * Display the chooser dialog using the compiled HTML from the mustache template * and binds onclick events for the generated comment options. * * @param {String} compiledSource The compiled HTML from the mustache template * @param {Array} comments Array containing comments. */ function displayChooserDialog(compiledSource, comments) { var titleLabel = ''; var cancelButtonId = 'comment-chooser-' + criterionId + '-cancel'; var cancelButton = ''; // Set dialog's body content. var chooserDialog = new M.core.dialogue({ modal: true, headerContent: titleLabel, bodyContent: compiledSource, footerContent: cancelButton, focusAfterHide: '#' + remarkId, id: "comments-chooser-dialog-" + criterionId }); // Bind click event to the cancel button. $("#" + cancelButtonId).click(function() { chooserDialog.hide(); }); // Loop over each comment item and bind click events. $.each(comments, function(index, comment) { var commentOptionId = '#comment-option-' + criterionId + '-' + comment.id; // Delegate click event for the generated option link. $(commentOptionId).click(function() { var remarkTextArea = $('#' + remarkId); var remarkText = remarkTextArea.val(); // Add line break if the current value of the remark text is not empty. if (remarkText.trim() !== '') { remarkText += '\n'; } remarkText += comment.description; remarkTextArea.val(remarkText); chooserDialog.hide(); }); // Handle keypress on list items. $(document).off('keypress', commentOptionId).on('keypress', commentOptionId, function() { var keyCode = event.which || event.keyCode; // Trigger click event when user presses space. if (keyCode === keycodes.space) { $(commentOptionId).click(); } }); }); // Destroy the dialog when it is hidden to allow the grading section to // be loaded as a fragment multiple times within the same page. chooserDialog.after('visibleChange', function(e) { // Going from visible to hidden. if (e.prevVal && !e.newVal) { this.destroy(); } }, chooserDialog); // Show dialog. chooserDialog.show(); } /** * Generates the comments chooser dialog from the grading_form/comment_chooser mustache template. */ function generateCommentsChooser() { // Template context. var context = { criterionId: criterionId, comments: commentOptions }; // Render the template and display the comment chooser dialog. templates.render('gradingform_guide/comment_chooser', context) .done(function(compiledSource) { displayChooserDialog(compiledSource, commentOptions); }) .fail(notification.exception); } // Bind click event for the comments chooser button. $("#" + buttonId).click(function(e) { e.preventDefault(); generateCommentsChooser(); }); } }; }); PK'\o6form/guide/amd/build/grades/grader/gradingpanel.min.jsnu[define("gradingform_guide/grades/grader/gradingpanel",["exports","core/ajax","core_grades/grades/grader/gradingpanel/normalise","core_grades/grades/grader/gradingpanel/comparison","jquery"],(function(_exports,_ajax,_normalise,_comparison,_jquery){var obj; /** * Grading panel for gradingform_guide. * * @module gradingform_guide/grades/grader/gradingpanel * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.storeCurrentGrade=_exports.fetchCurrentGrade=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.fetchCurrentGrade=(component,contextid,itemname,gradeduserid)=>(0,_ajax.call)([{methodname:"gradingform_guide_grader_gradingpanel_fetch",args:{component:component,contextid:contextid,itemname:itemname,gradeduserid:gradeduserid}}])[0];_exports.storeCurrentGrade=async(component,contextid,itemname,gradeduserid,notifyUser,rootNode)=>{const form=rootNode.querySelector("form");return!0===(0,_comparison.compareData)(form)?(0,_normalise.normaliseResult)(await(0,_ajax.call)([{methodname:"gradingform_guide_grader_gradingpanel_store",args:{component:component,contextid:contextid,itemname:itemname,gradeduserid:gradeduserid,notifyuser:notifyUser,formdata:(0,_jquery.default)(form).serialize()}}])[0]):""}})); //# sourceMappingURL=gradingpanel.min.js.mapPK'\[ :form/guide/amd/build/grades/grader/gradingpanel.min.js.mapnu[{"version":3,"file":"gradingpanel.min.js","sources":["../../../src/grades/grader/gradingpanel.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Grading panel for gradingform_guide.\n *\n * @module gradingform_guide/grades/grader/gradingpanel\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport {normaliseResult} from 'core_grades/grades/grader/gradingpanel/normalise';\nimport {compareData} from 'core_grades/grades/grader/gradingpanel/comparison';\n\n// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()\nimport jQuery from 'jquery';\n\n/**\n * For a given component, contextid, itemname & gradeduserid we can fetch the currently assigned grade.\n *\n * @param {String} component\n * @param {Number} contextid\n * @param {String} itemname\n * @param {Number} gradeduserid\n *\n * @returns {Promise}\n */\nexport const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => {\n return fetchMany([{\n methodname: `gradingform_guide_grader_gradingpanel_fetch`,\n args: {\n component,\n contextid,\n itemname,\n gradeduserid,\n },\n }])[0];\n};\n\n/**\n * For a given component, contextid, itemname & gradeduserid we can store the currently assigned grade in a given form.\n *\n * @param {String} component\n * @param {Number} contextid\n * @param {String} itemname\n * @param {Number} gradeduserid\n * @param {Boolean} notifyUser\n * @param {HTMLElement} rootNode\n *\n * @returns {Promise}\n */\nexport const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, notifyUser, rootNode) => {\n const form = rootNode.querySelector('form');\n\n if (compareData(form) === true) {\n return normaliseResult(await fetchMany([{\n methodname: `gradingform_guide_grader_gradingpanel_store`,\n args: {\n component,\n contextid,\n itemname,\n gradeduserid,\n notifyuser: notifyUser,\n formdata: jQuery(form).serialize(),\n },\n }])[0]);\n } else {\n return '';\n }\n};\n"],"names":["component","contextid","itemname","gradeduserid","methodname","args","async","notifyUser","rootNode","form","querySelector","notifyuser","formdata","serialize"],"mappings":";;;;;;;6MAwCiC,CAACA,UAAWC,UAAWC,SAAUC,gBACvD,cAAU,CAAC,CACdC,yDACAC,KAAM,CACFL,UAAAA,UACAC,UAAAA,UACAC,SAAAA,SACAC,aAAAA,iBAEJ,8BAeyBG,MAAMN,UAAWC,UAAWC,SAAUC,aAAcI,WAAYC,kBACvFC,KAAOD,SAASE,cAAc,eAEV,KAAtB,2BAAYD,OACL,oCAAsB,cAAU,CAAC,CACpCL,yDACAC,KAAM,CACFL,UAAAA,UACAC,UAAAA,UACAC,SAAAA,SACAC,aAAAA,aACAQ,WAAYJ,WACZK,UAAU,mBAAOH,MAAMI,gBAE3B,IAEG"}PK'\It?form/guide/amd/build/grades/grader/gradingpanel/comments.min.jsnu[define("gradingform_guide/grades/grader/gradingpanel/comments",["exports","./comments/selectors"],(function(_exports,_selectors){var obj; /** * Grading panel frequently used comments selector. * * @module gradingform_guide/grades/grader/gradingpanel/comments * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_selectors=(obj=_selectors)&&obj.__esModule?obj:{default:obj};_exports.init=rootId=>{document.querySelector("#".concat(rootId)).addEventListener("click",(e=>{if(!e.target.matches(_selectors.default.frequentComment))return;e.preventDefault();const clicked=e.target.closest(_selectors.default.frequentComment),remark=clicked.closest(_selectors.default.criterion).querySelector(_selectors.default.remark);remark&&(remark.value.trim()?remark.value+="\n".concat(clicked.innerHTML):remark.value+=clicked.innerHTML)}))}})); //# sourceMappingURL=comments.min.js.mapPK'\S Cform/guide/amd/build/grades/grader/gradingpanel/comments.min.js.mapnu[{"version":3,"file":"comments.min.js","sources":["../../../../src/grades/grader/gradingpanel/comments.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Grading panel frequently used comments selector.\n *\n * @module gradingform_guide/grades/grader/gradingpanel/comments\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Selectors from './comments/selectors';\n\n/**\n * Manage the frequently used comments in the Marking Guide form.\n *\n * @param {String} rootId\n */\nexport const init = (rootId) => {\n const rootNode = document.querySelector(`#${rootId}`);\n\n rootNode.addEventListener('click', (e) => {\n if (!e.target.matches(Selectors.frequentComment)) {\n return;\n }\n\n e.preventDefault();\n\n const clicked = e.target.closest(Selectors.frequentComment);\n const criterion = clicked.closest(Selectors.criterion);\n const remark = criterion.querySelector(Selectors.remark);\n\n if (!remark) {\n return;\n }\n\n // Either append the comment to an existing comment or set it as the comment.\n if (remark.value.trim()) {\n remark.value += `\\n${clicked.innerHTML}`;\n } else {\n remark.value += clicked.innerHTML;\n }\n });\n};\n"],"names":["rootId","document","querySelector","addEventListener","e","target","matches","Selectors","frequentComment","preventDefault","clicked","closest","remark","criterion","value","trim","innerHTML"],"mappings":";;;;;;;8JA8BqBA,SACAC,SAASC,yBAAkBF,SAEnCG,iBAAiB,SAAUC,QAC3BA,EAAEC,OAAOC,QAAQC,mBAAUC,wBAIhCJ,EAAEK,uBAEIC,QAAUN,EAAEC,OAAOM,QAAQJ,mBAAUC,iBAErCI,OADYF,QAAQC,QAAQJ,mBAAUM,WACnBX,cAAcK,mBAAUK,QAE5CA,SAKDA,OAAOE,MAAMC,OACbH,OAAOE,mBAAcJ,QAAQM,WAE7BJ,OAAOE,OAASJ,QAAQM"}PK'\PMform/guide/amd/build/grades/grader/gradingpanel/comments/selectors.min.js.mapnu[{"version":3,"file":"selectors.min.js","sources":["../../../../../src/grades/grader/gradingpanel/comments/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Define all of the selectors we will be using on the Marking Guide interface.\n *\n * @module gradingform_guide/grades/grader/gradingpanel/comments/selectors\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n frequentComment: '[data-gradingform_guide-role=\"frequent-comment\"]',\n criterion: '[data-gradingform-guide-role=\"criterion\"]',\n remark: '[data-gradingform-guide-role=\"remark\"]',\n};\n"],"names":["frequentComment","criterion","remark"],"mappings":"iNAsBe,CACXA,gBAAiB,mDACjBC,UAAW,4CACXC,OAAQ"}PK'\\iIform/guide/amd/build/grades/grader/gradingpanel/comments/selectors.min.jsnu[define("gradingform_guide/grades/grader/gradingpanel/comments/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={frequentComment:'[data-gradingform_guide-role="frequent-comment"]',criterion:'[data-gradingform-guide-role="criterion"]',remark:'[data-gradingform-guide-role="remark"]'},_exports.default})); //# sourceMappingURL=selectors.min.js.mapPK'\  /form/guide/amd/build/comment_chooser.min.js.mapnu[{"version":3,"file":"comment_chooser.min.js","sources":["../src/comment_chooser.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * AMD code for the frequently used comments chooser for the marking guide grading form.\n *\n * @module gradingform_guide/comment_chooser\n * @copyright 2015 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/templates', 'core/key_codes', 'core/notification', 'core/yui'],\n function($, templates, keycodes, notification) {\n\n // Private variables and functions.\n\n return /** @alias module:gradingform_guide/comment_chooser */ {\n // Public variables and functions.\n /**\n * Initialises the module.\n *\n * Basically, it performs the binding and handling of the button click event for\n * the 'Insert frequently used comment' button.\n *\n * @param {Integer} criterionId The criterion ID.\n * @param {String} buttonId The element ID of the button which the handler will be bound to.\n * @param {String} remarkId The element ID of the remark text area where the text of the selected comment will be copied to.\n * @param {Array} commentOptions The array of frequently used comments to be used as options.\n */\n initialise: function(criterionId, buttonId, remarkId, commentOptions) {\n /**\n * Display the chooser dialog using the compiled HTML from the mustache template\n * and binds onclick events for the generated comment options.\n *\n * @param {String} compiledSource The compiled HTML from the mustache template\n * @param {Array} comments Array containing comments.\n */\n function displayChooserDialog(compiledSource, comments) {\n var titleLabel = '';\n var cancelButtonId = 'comment-chooser-' + criterionId + '-cancel';\n var cancelButton = '';\n\n // Set dialog's body content.\n var chooserDialog = new M.core.dialogue({\n modal: true,\n headerContent: titleLabel,\n bodyContent: compiledSource,\n footerContent: cancelButton,\n focusAfterHide: '#' + remarkId,\n id: \"comments-chooser-dialog-\" + criterionId\n });\n\n // Bind click event to the cancel button.\n $(\"#\" + cancelButtonId).click(function() {\n chooserDialog.hide();\n });\n\n // Loop over each comment item and bind click events.\n $.each(comments, function(index, comment) {\n var commentOptionId = '#comment-option-' + criterionId + '-' + comment.id;\n\n // Delegate click event for the generated option link.\n $(commentOptionId).click(function() {\n var remarkTextArea = $('#' + remarkId);\n var remarkText = remarkTextArea.val();\n\n // Add line break if the current value of the remark text is not empty.\n if (remarkText.trim() !== '') {\n remarkText += '\\n';\n }\n remarkText += comment.description;\n\n remarkTextArea.val(remarkText);\n\n chooserDialog.hide();\n });\n\n // Handle keypress on list items.\n $(document).off('keypress', commentOptionId).on('keypress', commentOptionId, function() {\n var keyCode = event.which || event.keyCode;\n\n // Trigger click event when user presses space.\n if (keyCode === keycodes.space) {\n $(commentOptionId).click();\n }\n });\n });\n\n // Destroy the dialog when it is hidden to allow the grading section to\n // be loaded as a fragment multiple times within the same page.\n chooserDialog.after('visibleChange', function(e) {\n // Going from visible to hidden.\n if (e.prevVal && !e.newVal) {\n this.destroy();\n }\n }, chooserDialog);\n\n // Show dialog.\n chooserDialog.show();\n }\n\n /**\n * Generates the comments chooser dialog from the grading_form/comment_chooser mustache template.\n */\n function generateCommentsChooser() {\n // Template context.\n var context = {\n criterionId: criterionId,\n comments: commentOptions\n };\n\n // Render the template and display the comment chooser dialog.\n templates.render('gradingform_guide/comment_chooser', context)\n .done(function(compiledSource) {\n displayChooserDialog(compiledSource, commentOptions);\n })\n .fail(notification.exception);\n }\n\n // Bind click event for the comments chooser button.\n $(\"#\" + buttonId).click(function(e) {\n e.preventDefault();\n generateCommentsChooser();\n });\n }\n };\n});\n"],"names":["define","$","templates","keycodes","notification","initialise","criterionId","buttonId","remarkId","commentOptions","generateCommentsChooser","context","comments","render","done","compiledSource","titleLabel","M","util","get_string","cancelButtonId","cancelButton","chooserDialog","core","dialogue","modal","headerContent","bodyContent","footerContent","focusAfterHide","id","click","hide","each","index","comment","commentOptionId","remarkTextArea","remarkText","val","trim","description","document","off","on","event","which","keyCode","space","after","e","prevVal","newVal","destroy","show","displayChooserDialog","fail","exception","preventDefault"],"mappings":";;;;;;;AAsBAA,2CAAO,CAAC,SAAU,iBAAkB,iBAAkB,oBAAqB,aACnE,SAASC,EAAGC,UAAWC,SAAUC,oBAIyB,CAa1DC,WAAY,SAASC,YAAaC,SAAUC,SAAUC,yBA2EzCC,8BAEDC,QAAU,CACVL,YAAaA,YACbM,SAAUH,gBAIdP,UAAUW,OAAO,oCAAqCF,SACjDG,MAAK,SAASC,0BA5EOA,eAAgBH,cACtCI,WAAa,UAAYC,EAAEC,KAAKC,WAAW,gBAAiB,qBAAuB,WACnFC,eAAiB,mBAAqBd,YAAc,UACpDe,aAAe,eAAiBD,eAAiB,KAAOH,EAAEC,KAAKC,WAAW,SAAU,UAAY,YAGhGG,cAAgB,IAAIL,EAAEM,KAAKC,SAAS,CACpCC,OAAO,EACPC,cAAeV,WACfW,YAAaZ,eACba,cAAeP,aACfQ,eAAgB,IAAMrB,SACtBsB,GAAI,2BAA6BxB,cAIrCL,EAAE,IAAMmB,gBAAgBW,OAAM,WAC1BT,cAAcU,UAIlB/B,EAAEgC,KAAKrB,UAAU,SAASsB,MAAOC,aACzBC,gBAAkB,mBAAqB9B,YAAc,IAAM6B,QAAQL,GAGvE7B,EAAEmC,iBAAiBL,OAAM,eACjBM,eAAiBpC,EAAE,IAAMO,UACzB8B,WAAaD,eAAeE,MAGN,KAAtBD,WAAWE,SACXF,YAAc,MAElBA,YAAcH,QAAQM,YAEtBJ,eAAeE,IAAID,YAEnBhB,cAAcU,UAIlB/B,EAAEyC,UAAUC,IAAI,WAAYP,iBAAiBQ,GAAG,WAAYR,iBAAiB,YAC3DS,MAAMC,OAASD,MAAME,WAGnB5C,SAAS6C,OACrB/C,EAAEmC,iBAAiBL,cAO/BT,cAAc2B,MAAM,iBAAiB,SAASC,GAEtCA,EAAEC,UAAYD,EAAEE,aACXC,YAEV/B,eAGHA,cAAcgC,OAgBNC,CAAqBxC,eAAgBN,mBAExC+C,KAAKpD,aAAaqD,WAI3BxD,EAAE,IAAMM,UAAUwB,OAAM,SAASmB,GAC7BA,EAAEQ,iBACFhD"}PK'\ڮ#'+form/guide/amd/build/comment_chooser.min.jsnu[/** * AMD code for the frequently used comments chooser for the marking guide grading form. * * @module gradingform_guide/comment_chooser * @copyright 2015 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("gradingform_guide/comment_chooser",["jquery","core/templates","core/key_codes","core/notification","core/yui"],(function($,templates,keycodes,notification){return{initialise:function(criterionId,buttonId,remarkId,commentOptions){function generateCommentsChooser(){var context={criterionId:criterionId,comments:commentOptions};templates.render("gradingform_guide/comment_chooser",context).done((function(compiledSource){!function(compiledSource,comments){var titleLabel="",cancelButtonId="comment-chooser-"+criterionId+"-cancel",cancelButton='",chooserDialog=new M.core.dialogue({modal:!0,headerContent:titleLabel,bodyContent:compiledSource,footerContent:cancelButton,focusAfterHide:"#"+remarkId,id:"comments-chooser-dialog-"+criterionId});$("#"+cancelButtonId).click((function(){chooserDialog.hide()})),$.each(comments,(function(index,comment){var commentOptionId="#comment-option-"+criterionId+"-"+comment.id;$(commentOptionId).click((function(){var remarkTextArea=$("#"+remarkId),remarkText=remarkTextArea.val();""!==remarkText.trim()&&(remarkText+="\n"),remarkText+=comment.description,remarkTextArea.val(remarkText),chooserDialog.hide()})),$(document).off("keypress",commentOptionId).on("keypress",commentOptionId,(function(){(event.which||event.keyCode)===keycodes.space&&$(commentOptionId).click()}))})),chooserDialog.after("visibleChange",(function(e){e.prevVal&&!e.newVal&&this.destroy()}),chooserDialog),chooserDialog.show()}(compiledSource,commentOptions)})).fail(notification.exception)}$("#"+buttonId).click((function(e){e.preventDefault(),generateCommentsChooser()}))}}})); //# sourceMappingURL=comment_chooser.min.js.mapPK'\᷐[;[;Cform/guide/tests/grades/grader/gradingpanel/external/fetch_test.phpnu[. declare(strict_types = 1); namespace gradingform_guide\grades\grader\gradingpanel\external; use advanced_testcase; use coding_exception; use core_grades\component_gradeitem; use core_external\external_api; use mod_forum\local\entities\forum as forum_entity; use moodle_exception; /** * Unit tests for core_grades\component_gradeitems; * * @package gradingform_guide * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class fetch_test extends advanced_testcase { /** * Ensure that an execute with an invalid component is rejected. */ public function test_execute_invalid_component(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(coding_exception::class); $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component"); fetch::execute('mod_invalid', 1, 'foo', 2); } /** * Ensure that an execute with an invalid itemname on a valid component is rejected. */ public function test_execute_invalid_itemname(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(coding_exception::class); $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component"); fetch::execute('mod_forum', 1, 'foo', 2); } /** * Ensure that an execute against a different grading method is rejected. */ public function test_execute_incorrect_type(): void { $this->resetAfterTest(); $forum = $this->get_forum_instance([ // Negative numbers mean a scale. 'grade_forum' => 5, ]); $course = $forum->get_course_record(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setUser($teacher); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $this->expectException(moodle_exception::class); $this->expectExceptionMessage("not configured for advanced grading with a marking guide"); fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id); } /** * Ensure that an execute against the correct grading method returns the current state of the user. */ public function test_execute_fetch_empty(): void { $this->resetAfterTest(); [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, ] = $this->get_test_data(); $this->setUser($teacher); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id); $result = external_api::clean_returnvalue(fetch::execute_returns(), $result); $this->assertIsArray($result); $this->assertArrayHasKey('templatename', $result); $this->assertEquals('gradingform_guide/grades/grader/gradingpanel', $result['templatename']); $this->assertArrayHasKey('warnings', $result); $this->assertIsArray($result['warnings']); $this->assertEmpty($result['warnings']); // Test the grade array items. $this->assertArrayHasKey('grade', $result); $this->assertIsArray($result['grade']); $this->assertIsInt($result['grade']['timecreated']); $this->assertArrayHasKey('timemodified', $result['grade']); $this->assertIsInt($result['grade']['timemodified']); $this->assertArrayHasKey('usergrade', $result['grade']); $this->assertEquals('- / 100.00', $result['grade']['usergrade']); $this->assertArrayHasKey('maxgrade', $result['grade']); $this->assertIsInt($result['grade']['maxgrade']); $this->assertEquals(100, $result['grade']['maxgrade']); $this->assertArrayHasKey('gradedby', $result['grade']); $this->assertEquals(null, $result['grade']['gradedby']); $this->assertArrayHasKey('criterion', $result['grade']); $criteria = $result['grade']['criterion']; $this->assertCount(count($definition->guide_criteria), $criteria); foreach ($criteria as $criterion) { $this->assertArrayHasKey('id', $criterion); $criterionid = $criterion['id']; $sourcecriterion = $definition->guide_criteria[$criterionid]; $this->assertArrayHasKey('name', $criterion); $this->assertEquals($sourcecriterion['shortname'], $criterion['name']); $this->assertArrayHasKey('maxscore', $criterion); $this->assertEquals($sourcecriterion['maxscore'], $criterion['maxscore']); $this->assertArrayHasKey('description', $criterion); $this->assertEquals($sourcecriterion['description'], $criterion['description']); $this->assertArrayHasKey('descriptionmarkers', $criterion); $this->assertEquals($sourcecriterion['descriptionmarkers'], $criterion['descriptionmarkers']); $this->assertArrayHasKey('score', $criterion); $this->assertEmpty($criterion['score']); $this->assertArrayHasKey('remark', $criterion); $this->assertEmpty($criterion['remark']); } } /** * Ensure that an execute against the correct grading method returns the current state of the user. */ public function test_execute_fetch_graded(): void { $this->resetAfterTest(); [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, ] = $this->get_test_data(); $this->execute_and_assert_fetch($forum, $controller, $definition, $teacher, $teacher, $student); } /** * Class mates should not get other's grades. */ public function test_execute_fetch_does_not_return_data_to_other_students(): void { $this->resetAfterTest(); [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, 'course' => $course, ] = $this->get_test_data(); $evilstudent = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->expectException(\required_capability_exception::class); $this->execute_and_assert_fetch($forum, $controller, $definition, $evilstudent, $teacher, $student); } /** * Grades can be returned to graded user. */ public function test_execute_fetch_return_data_to_graded_user(): void { $this->resetAfterTest(); [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, ] = $this->get_test_data(); $this->execute_and_assert_fetch($forum, $controller, $definition, $student, $teacher, $student); } /** * Executes and performs all the assertions of the fetch method with the given parameters. */ private function execute_and_assert_fetch($forum, $controller, $definition, $fetcheruser, $grader, $gradeduser) { $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); $this->setUser($grader); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $grade = $gradeitem->get_grade_for_user($gradeduser, $grader); $instance = $gradeitem->get_advanced_grading_instance($grader, $grade); $submissiondata = $guidegenerator->get_test_form_data($controller, (int) $gradeduser->id, 10, 'Propper good speling', 0, 'ASCII art is not a picture' ); $gradeitem->store_grade_from_formdata($gradeduser, $grader, (object) [ 'instanceid' => $instance->get_id(), 'advancedgrading' => $submissiondata, ]); $this->setUser($fetcheruser); // Set up some items we need to return on other interfaces. $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $gradeduser->id); $result = external_api::clean_returnvalue(fetch::execute_returns(), $result); $this->assertIsArray($result); $this->assertArrayHasKey('templatename', $result); $this->assertEquals('gradingform_guide/grades/grader/gradingpanel', $result['templatename']); $this->assertArrayHasKey('warnings', $result); $this->assertIsArray($result['warnings']); $this->assertEmpty($result['warnings']); // Test the grade array items. $this->assertArrayHasKey('grade', $result); $this->assertIsArray($result['grade']); $this->assertIsInt($result['grade']['timecreated']); $this->assertArrayHasKey('timemodified', $result['grade']); $this->assertIsInt($result['grade']['timemodified']); $this->assertArrayHasKey('usergrade', $result['grade']); $this->assertEquals('25.00 / 100.00', $result['grade']['usergrade']); $this->assertArrayHasKey('maxgrade', $result['grade']); $this->assertIsInt($result['grade']['maxgrade']); $this->assertEquals(100, $result['grade']['maxgrade']); $this->assertArrayHasKey('gradedby', $result['grade']); $this->assertEquals(fullname($grader), $result['grade']['gradedby']); $this->assertArrayHasKey('criterion', $result['grade']); $criteria = $result['grade']['criterion']; $this->assertCount(count($definition->guide_criteria), $criteria); foreach ($criteria as $criterion) { $this->assertArrayHasKey('id', $criterion); $criterionid = $criterion['id']; $sourcecriterion = $definition->guide_criteria[$criterionid]; $this->assertArrayHasKey('name', $criterion); $this->assertEquals($sourcecriterion['shortname'], $criterion['name']); $this->assertArrayHasKey('maxscore', $criterion); $this->assertEquals($sourcecriterion['maxscore'], $criterion['maxscore']); $this->assertArrayHasKey('description', $criterion); $this->assertEquals($sourcecriterion['description'], $criterion['description']); $this->assertArrayHasKey('descriptionmarkers', $criterion); $this->assertEquals($sourcecriterion['descriptionmarkers'], $criterion['descriptionmarkers']); $this->assertArrayHasKey('score', $criterion); $this->assertArrayHasKey('remark', $criterion); } $this->assertEquals(10, $criteria[0]['score']); $this->assertEquals('Propper good speling', $criteria[0]['remark']); $this->assertEquals(0, $criteria[1]['score']); $this->assertEquals('ASCII art is not a picture', $criteria[1]['remark']); } /** * Get a forum instance. * * @param array $config * @return forum_entity */ protected function get_forum_instance(array $config = []): forum_entity { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id, 'grade_forum' => 100])); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_forum_vault(); return $vault->get_from_id((int) $forum->id); } /** * Get test data for forums graded using a marking guide. * * @return array */ protected function get_test_data(): array { global $DB; $this->resetAfterTest(); $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); $forum = $this->get_forum_instance(); $course = $forum->get_course_record(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setUser($teacher); $controller = $guidegenerator->get_test_guide($forum->get_context(), 'forum', 'forum'); $definition = $controller->get_definition(); // In the situation of mod_forum this would be the id from forum_grades. $itemid = 1; $instance = $controller->create_instance($student->id, $itemid); $data = $this->get_test_form_data( $controller, $itemid, 5, 'This user made several mistakes.', 10, 'This user has two pictures.' ); // Update this instance with data. $instance->update($data); return [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, 'course' => $course, ]; } /** * Fetch a set of sample data. * * @param \gradingform_guide_controller $controller * @param int $itemid * @param float $spellingscore * @param string $spellingremark * @param float $picturescore * @param string $pictureremark * @return array */ protected function get_test_form_data( \gradingform_guide_controller $controller, int $itemid, float $spellingscore, string $spellingremark, float $picturescore, string $pictureremark ): array { $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); return $guidegenerator->get_test_form_data( $controller, $itemid, $spellingscore, $spellingremark, $picturescore, $pictureremark ); } } PK'\,G M&M&Cform/guide/tests/grades/grader/gradingpanel/external/store_test.phpnu[. declare(strict_types = 1); namespace gradingform_guide\grades\grader\gradingpanel\external; use advanced_testcase; use coding_exception; use core_grades\component_gradeitem; use core_external\external_api; use mod_forum\local\entities\forum as forum_entity; use moodle_exception; /** * Unit tests for core_grades\component_gradeitems; * * @package gradingform_guide * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class store_test extends advanced_testcase { /** * Ensure that an execute with an invalid component is rejected. */ public function test_execute_invalid_component(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(coding_exception::class); $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component"); store::execute('mod_invalid', 1, 'foo', 2, false, 'formdata'); } /** * Ensure that an execute with an invalid itemname on a valid component is rejected. */ public function test_execute_invalid_itemname(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(coding_exception::class); $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component"); store::execute('mod_forum', 1, 'foo', 2, false, 'formdata'); } /** * Ensure that an execute against a different grading method is rejected. */ public function test_execute_incorrect_type(): void { $this->resetAfterTest(); $forum = $this->get_forum_instance([ 'grade_forum' => 5, ]); $course = $forum->get_course_record(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setUser($teacher); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $this->expectException(moodle_exception::class); $this->expectExceptionMessage("not configured for advanced grading with a marking guide"); store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, false, 'formdata'); } /** * Ensure that an execute against a different grading method is rejected. */ public function test_execute_disabled(): void { $this->resetAfterTest(); $forum = $this->get_forum_instance(); $course = $forum->get_course_record(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setUser($teacher); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $this->expectException(moodle_exception::class); $this->expectExceptionMessage("Grading is not enabled"); store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, false, 'formdata'); } /** * Ensure that an execute against the correct grading method returns the current state of the user. */ public function test_execute_store_graded(): void { $this->resetAfterTest(); $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, ] = $this->get_test_data(); $this->setUser($teacher); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $grade = $gradeitem->get_grade_for_user($student, $teacher); $instance = $gradeitem->get_advanced_grading_instance($teacher, $grade); $submissiondata = $guidegenerator->get_test_form_data($controller, (int) $student->id, 10, 'Propper good speling', 0, 'ASCII art is not a picture' ); $formdata = http_build_query((object) [ 'instanceid' => $instance->get_id(), 'advancedgrading' => $submissiondata, ], '', '&'); $result = store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, false, $formdata); $result = external_api::clean_returnvalue(store::execute_returns(), $result); $this->assertIsArray($result); $this->assertArrayHasKey('templatename', $result); $this->assertEquals('gradingform_guide/grades/grader/gradingpanel', $result['templatename']); $this->assertArrayHasKey('warnings', $result); $this->assertIsArray($result['warnings']); $this->assertEmpty($result['warnings']); // Test the grade array items. $this->assertArrayHasKey('grade', $result); $this->assertIsArray($result['grade']); $this->assertIsInt($result['grade']['timecreated']); $this->assertArrayHasKey('timemodified', $result['grade']); $this->assertIsInt($result['grade']['timemodified']); $this->assertArrayHasKey('usergrade', $result['grade']); $this->assertEquals('0.50 / 2.00', $result['grade']['usergrade']); $this->assertArrayHasKey('maxgrade', $result['grade']); $this->assertIsInt($result['grade']['maxgrade']); $this->assertEquals(2, $result['grade']['maxgrade']); $this->assertArrayHasKey('gradedby', $result['grade']); $this->assertEquals(fullname($teacher), $result['grade']['gradedby']); $this->assertArrayHasKey('criterion', $result['grade']); $criteria = $result['grade']['criterion']; $this->assertCount(count($definition->guide_criteria), $criteria); foreach ($criteria as $criterion) { $this->assertArrayHasKey('id', $criterion); $criterionid = $criterion['id']; $sourcecriterion = $definition->guide_criteria[$criterionid]; $this->assertArrayHasKey('name', $criterion); $this->assertEquals($sourcecriterion['shortname'], $criterion['name']); $this->assertArrayHasKey('maxscore', $criterion); $this->assertEquals($sourcecriterion['maxscore'], $criterion['maxscore']); $this->assertArrayHasKey('description', $criterion); $this->assertEquals($sourcecriterion['description'], $criterion['description']); $this->assertArrayHasKey('descriptionmarkers', $criterion); $this->assertEquals($sourcecriterion['descriptionmarkers'], $criterion['descriptionmarkers']); $this->assertArrayHasKey('score', $criterion); $this->assertArrayHasKey('remark', $criterion); } $this->assertEquals(10, $criteria[0]['score']); $this->assertEquals('Propper good speling', $criteria[0]['remark']); $this->assertEquals(0, $criteria[1]['score']); $this->assertEquals('ASCII art is not a picture', $criteria[1]['remark']); } /** * Get a forum instance. * * @param array $config * @return forum_entity */ protected function get_forum_instance(array $config = []): forum_entity { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id])); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_forum_vault(); return $vault->get_from_id((int) $forum->id); } /** * Get test data for forums graded using a marking guide. * * @return array */ protected function get_test_data(): array { global $DB; $this->resetAfterTest(); $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); $forum = $this->get_forum_instance(); $course = $forum->get_course_record(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setUser($teacher); $controller = $guidegenerator->get_test_guide($forum->get_context(), 'forum', 'forum'); $definition = $controller->get_definition(); $DB->set_field('forum', 'grade_forum', count($definition->guide_criteria), ['id' => $forum->get_id()]); return [ 'forum' => $forum, 'controller' => $controller, 'definition' => $definition, 'student' => $student, 'teacher' => $teacher, ]; } } PK'\ "form/guide/tests/generator/lib.phpnu[. /** * Generator for the gradingforum_guide plugin. * * @package gradingform_guide * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/guide.php'); require_once(__DIR__ . '/criterion.php'); use tests\gradingform_guide\generator\guide; use tests\gradingform_guide\generator\criterion; /** * Generator for the gradingforum_guide plugintype. * * @package gradingform_guide * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradingform_guide_generator extends component_generator_base { /** * Create an instance of a marking guide. * * @param context $context * @param string $component * @param string $area * @param string $name * @param string $description * @param array $criteria The list of criteria to add to the generated guide * @return gradingform_guide_controller */ public function create_instance( context $context, string $component, string $area, string $name, string $description, array $criteria ): gradingform_guide_controller { global $USER; if ($USER->id === 0) { throw new \coding_exception('Creation of a guide must currently be run as a user.'); } // Fetch the controller for this context/component/area. $generator = \testing_util::get_data_generator(); $gradinggenerator = $generator->get_plugin_generator('core_grading'); $controller = $gradinggenerator->create_instance($context, $component, $area, 'guide'); // Generate a definition for the supplied guide. $guide = $this->get_guide($name, $description); foreach ($criteria as $name => $options) { $guide->add_criteria($this->get_criterion( $name, $options['description'], $options['descriptionmarkers'], $options['maxscore'] )); } // Update the controller wih the guide definition. $controller->update_definition($guide->get_definition()); return $controller; } /** * Get a new guide for use with the guide controller. * * Note: This is just a helper class used to build a new definition. It does not persist the data. * * @param string $name * @param string $description * @return generator_guide */ protected function get_guide(string $name, string $description): guide { return new \tests\gradingform_guide\generator\guide($name, $description); } /** * Get a new criterion for use with a guide. * * Note: This is just a helper class used to build a new definition. It does not persist the data. * * @param string $shortname The shortname for the criterion * @param string $description The description for the criterion * @param string $descriptionmarkers The description for the marker for this criterion * @param float $maxscore The maximum score possible for this criterion * @return criterion */ protected function get_criterion( string $shortname, string $description, string $descriptionmarkers, float $maxscore ): criterion { return new criterion($shortname, $description, $descriptionmarkers, $maxscore); } /** * Given a controller instance, fetch the level and criterion information for the specified values. * * @param gradingform_controller $controller * @param string $shortname The shortname to match the criterion on * @return stdClass */ public function get_criterion_for_values(gradingform_controller $controller, string $shortname): ?stdClass { $definition = $controller->get_definition(); $criteria = $definition->guide_criteria; $criterion = array_reduce($criteria, function($carry, $criterion) use ($shortname) { if ($criterion['shortname'] === $shortname) { $carry = (object) $criterion; } return $carry; }, null); return $criterion; } /** * Get submitted form data * * @param gradingform_guide_controller $controller * @param int $itemid * @param array $values A set of array values where the array key is the name of the criterion, and the value is an * array with the desired score, and any remark. */ public function get_submitted_form_data(gradingform_guide_controller $controller, int $itemid, array $values): array { $result = [ 'itemid' => $itemid, 'criteria' => [], ]; foreach ($values as $criterionname => ['score' => $score, 'remark' => $remark]) { $criterion = $this->get_criterion_for_values($controller, $criterionname); $result['criteria'][$criterion->id] = [ 'score' => $score, 'remark' => $remark, ]; } return $result; } /** * Generate a guide controller with sample data required for testing of this class. * * @param context_module $context * @return gradingform_guide_controller */ public function get_test_guide( context_module $context, string $component = 'mod_assign', string $areaname = 'submission' ): gradingform_guide_controller { $generator = \testing_util::get_data_generator(); $gradinggenerator = $generator->get_plugin_generator('core_grading'); $controller = $gradinggenerator->create_instance($context, $component, $areaname, 'guide'); $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); $guide = $guidegenerator->get_guide('testguide', 'Description text'); $guide->add_criteria($guidegenerator->get_criterion( 'Spelling mistakes', 'Full marks will be given for no spelling mistakes.', 'Deduct 5 points per spelling mistake made.', 25 )); $guide->add_criteria($guidegenerator->get_criterion( 'Pictures', 'Full marks will be given for including 3 pictures.', 'Give 5 points for each picture present', 15 )); $controller->update_definition($guide->get_definition()); return $controller; } /** * Fetch a set of sample data. * * @param gradingform_guide_controller $controller * @param int $itemid * @param float $spellingscore * @param string $spellingremark * @param float $picturescore * @param string $pictureremark * @return array */ public function get_test_form_data( gradingform_guide_controller $controller, int $itemid, float $spellingscore, string $spellingremark, float $picturescore, string $pictureremark ): array { $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); return $guidegenerator->get_submitted_form_data($controller, $itemid, [ 'Spelling mistakes' => [ 'score' => $spellingscore, 'remark' => $spellingremark, ], 'Pictures' => [ 'score' => $picturescore, 'remark' => $pictureremark, ], ]); } } PK'\LZ $form/guide/tests/generator/guide.phpnu[. /** * Generator for the gradingforum_guide plugin. * * @package gradingform_guide * @category test * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tests\gradingform_guide\generator; use gradingform_controller; use stdClass; /** * Test guide. * * @package gradingform_guide * @category test * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class guide { /** @var array $criteria The criteria for this guide. */ protected $criteria = []; /** @var string The name of this guide. */ protected $name; /** @var string A description for this guide. */ protected $description; /** @var array The guide options. */ protected $options = []; /** * Create a new gradingform_guide_generator_criterion. * * @param string $name * @param string $description */ public function __construct(string $name, string $description) { $this->name = $name; $this->description = $description; $this->set_option('alwaysshowdefinition', 1); $this->set_option('showmarkspercriterionstudents', 1); } /** * Creates the guide using the appropriate APIs. */ public function get_definition(): stdClass { return (object) [ 'name' => $this->name, 'description_editor' => [ 'text' => $this->description, 'format' => FORMAT_HTML, 'itemid' => 1 ], 'guide' => [ 'criteria' => $this->get_critiera_as_array(), 'options' => $this->options, 'comments' => [], ], 'saveguide' => 'Continue', 'status' => gradingform_controller::DEFINITION_STATUS_READY, ]; } /** * Set an option for the rubric. * * @param string $key * @param mixed $value * @return self */ public function set_option(string $key, $value): self { $this->options[$key] = $value; return $this; } /** * Adds a criterion to the guide. * * @param criterion $criterion The criterion object (class below). * @return self */ public function add_criteria(criterion $criterion): self { $this->criteria[] = $criterion; return $this; } /** * Get the criteria as an array for use in creation. * * @return array */ protected function get_critiera_as_array(): array { $return = []; foreach ($this->criteria as $index => $criterion) { $return["NEWID{$index}"] = $criterion->get_all_values($index + 1); } return $return; } } PK'\5զ (form/guide/tests/generator/criterion.phpnu[. /** * Generator for the gradingforum_guide plugin. * * @package gradingform_guide * @category test * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tests\gradingform_guide\generator; /** * Convenience class to create guide criterion. * * @package gradingform_guide * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class criterion { /** @var string $shortname A shortened name of the criterion. */ private $shortname; /** @var string $description A description of the criterion. */ private $description; /** @var string $descriptionmarkers A description of the criterion for markers. */ private $descriptionmarkers; /** @var float Maximum score */ private $maxscore = 0; /** * Constructor for this test_criterion object * * @param string $shortname The shortname for the criterion * @param string $description The description for the criterion * @param string $descriptionmarkers The description for the marker for this criterion * @param float $maxscore The maximum score possible for this criterion */ public function __construct(string $shortname, string $description, string $descriptionmarkers, float $maxscore) { $this->shortname = $shortname; $this->description = $description; $this->descriptionmarkers = $descriptionmarkers; $this->maxscore = $maxscore; } /** * Get the description for this criterion. * * @return string */ public function get_description(): string { return $this->description; } /** * Get the description for markers of this criterion. * * @return string */ public function get_descriptionmarkers(): string { return $this->descriptionmarkers; } /** * Get the shortname for this criterion. * * @return string */ public function get_shortname(): string { return $this->shortname; } /** * Get the maxscore for this criterion. * * @return float */ public function get_maxscore(): float { return $this->maxscore; } /** * Get all values in an array for use when creating a new guide. * * @param int $sortorder * @return array */ public function get_all_values(int $sortorder): array { return [ 'sortorder' => $sortorder, 'shortname' => $this->get_shortname(), 'description' => $this->get_description(), 'descriptionmarkers' => $this->get_descriptionmarkers(), 'maxscore' => $this->get_maxscore(), ]; } } PK'\wm*form/guide/tests/privacy/provider_test.phpnu[. /** * Privacy tests for gradingform_guide. * * @package gradingform_guide * @category test * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace gradingform_guide\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; use core_privacy\tests\provider_testcase; use core_privacy\local\request\writer; use gradingform_guide\privacy\provider; /** * Privacy tests for gradingform_guide. * * @package gradingform_guide * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { /** * Ensure that export_user_preferences returns no data if the user has no data. */ public function test_export_user_preferences_not_defined(): void { $user = \core_user::get_user_by_username('admin'); provider::export_user_preferences($user->id); $writer = writer::with_context(\context_system::instance()); $this->assertFalse($writer->has_any_data()); } /** * Ensure that export_user_preferences returns single preferences. */ public function test_export_user_preferences(): void { $this->resetAfterTest(); // Define a user preference. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); set_user_preference('gradingform_guide-showmarkerdesc', 0, $user); set_user_preference('gradingform_guide-showstudentdesc', 1, $user); // Validate exported data. provider::export_user_preferences($user->id); $context = \context_user::instance($user->id); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); $prefs = $writer->get_user_preferences('gradingform_guide'); $this->assertCount(2, (array) $prefs); $this->assertEquals( get_string('privacy:metadata:preference:showstudentdesc', 'gradingform_guide'), $prefs->{'gradingform_guide-showstudentdesc'}->description ); $this->assertEquals(get_string('no'), $prefs->{'gradingform_guide-showmarkerdesc'}->value); $this->assertEquals(get_string('yes'), $prefs->{'gradingform_guide-showstudentdesc'}->value); } /** * Test the export of guide data. */ public function test_get_gradingform_export_data(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $module = $this->getDataGenerator()->create_module('assign', ['course' => $course]); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $modulecontext = \context_module::instance($module->cmid); $controller = $this->get_test_guide($modulecontext); // In the situation of mod_assign this would be the id from assign_grades. $itemid = 1; $instance = $controller->create_instance($user->id, $itemid); $data = $this->get_test_form_data( $controller, $itemid, 5, 'This user made several mistakes.', 10, 'This user has two pictures.' ); $instance->update($data); $instanceid = $instance->get_data('id'); // Let's try the method we are testing. provider::export_gradingform_instance_data($modulecontext, $instance->get_id(), ['Test']); $data = (array) writer::with_context($modulecontext)->get_data(['Test', 'Marking guide', $instanceid]); $this->assertCount(2, $data); $this->assertEquals('Spelling mistakes', $data['Spelling mistakes']->shortname); $this->assertEquals('This user made several mistakes.', $data['Spelling mistakes']->remark); $this->assertEquals('Pictures', $data['Pictures']->shortname); $this->assertEquals('This user has two pictures.', $data['Pictures']->remark); } /** * Test the deletion of guide user information via the instance ID. */ public function test_delete_gradingform_for_instances(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $module = $this->getDataGenerator()->create_module('assign', ['course' => $course]); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $modulecontext = \context_module::instance($module->cmid); $controller = $this->get_test_guide($modulecontext); // In the situation of mod_assign this would be the id from assign_grades. $itemid = 1; $instance = $controller->create_instance($user->id, $itemid); $data = $this->get_test_form_data( $controller, $itemid, 5, 'This user made several mistakes.', 10, 'This user has two pictures.' ); $instance->update($data); $instanceid = $instance->get_data('id'); $itemid = 2; $instance = $controller->create_instance($user->id, $itemid); $data = $this->get_test_form_data( $controller, $itemid, 25, 'This user made no mistakes.', 5, 'This user has one pictures.' ); $instance->update($data); $instanceid = $instance->get_data('id'); // Check how many records we have in the fillings table. $records = $DB->get_records('gradingform_guide_fillings'); $this->assertCount(4, $records); // Let's delete one of the instances (the last one would be the easiest). provider::delete_gradingform_for_instances([$instance->get_id()]); $records = $DB->get_records('gradingform_guide_fillings'); $this->assertCount(2, $records); foreach ($records as $record) { $this->assertNotEquals($instance->get_id(), $record->instanceid); } } /** * Generate a guide controller with sample data required for testing of this class. * * @param \context_module $context * @return \gradingform_guide_controller */ protected function get_test_guide(\context_module $context): \gradingform_guide_controller { $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); return $guidegenerator->get_test_guide($context); } /** * Fetch a set of sample data. * * @param \gradingform_guide_controller $controller * @param int $itemid * @param float $spellingscore * @param string $spellingremark * @param float $picturescore * @param string $pictureremark * @return array */ protected function get_test_form_data( \gradingform_guide_controller $controller, int $itemid, float $spellingscore, string $spellingremark, float $picturescore, string $pictureremark ): array { $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); return $guidegenerator->get_test_form_data( $controller, $itemid, $spellingscore, $spellingremark, $picturescore, $pictureremark ); } } PK'\uH% form/guide/tests/guide_test.phpnu[. namespace gradingform_guide; use gradingform_controller; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/grade/grading/lib.php'); require_once($CFG->dirroot . '/grade/grading/form/guide/lib.php'); /** * Test cases for the Marking Guide. * * @package gradingform_guide * @category test * @copyright 2015 Nikita Kalinin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class guide_test extends \advanced_testcase { /** * Unit test to get draft instance and create new instance. */ public function test_get_or_create_instance(): void { global $DB; $this->resetAfterTest(true); // Create fake areas. $fakearea = (object)array( 'contextid' => 1, 'component' => 'mod_assign', 'areaname' => 'submissions', 'activemethod' => 'guide' ); $fakearea1id = $DB->insert_record('grading_areas', $fakearea); $fakearea->contextid = 2; $fakearea2id = $DB->insert_record('grading_areas', $fakearea); // Create fake definitions. $fakedefinition = (object)array( 'areaid' => $fakearea1id, 'method' => 'guide', 'name' => 'fakedef', 'status' => gradingform_controller::DEFINITION_STATUS_READY, 'timecreated' => 0, 'usercreated' => 1, 'timemodified' => 0, 'usermodified' => 1, ); $fakedef1id = $DB->insert_record('grading_definitions', $fakedefinition); $fakedefinition->areaid = $fakearea2id; $fakedef2id = $DB->insert_record('grading_definitions', $fakedefinition); // Create fake guide instance in first area. $fakeinstance = (object)array( 'definitionid' => $fakedef1id, 'raterid' => 1, 'itemid' => 1, 'rawgrade' => null, 'status' => 0, 'feedback' => null, 'feedbackformat' => 0, 'timemodified' => 0 ); $fakeinstanceid = $DB->insert_record('grading_instances', $fakeinstance); $manager1 = get_grading_manager($fakearea1id); $manager2 = get_grading_manager($fakearea2id); $controller1 = $manager1->get_controller('guide'); $controller2 = $manager2->get_controller('guide'); $instance1 = $controller1->get_or_create_instance(0, 1, 1); $instance2 = $controller2->get_or_create_instance(0, 1, 1); // Definitions should not be the same. $this->assertEquals(false, $instance1->get_data('definitionid') == $instance2->get_data('definitionid')); } } PK'\6E,,#form/guide/tests/generator_test.phpnu[. /** * Generator testcase for the gradingforum_guide generator. * * @package gradingform_guide * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace gradingform_guide; use context_module; use gradingform_controller; use gradingform_guide_controller; /** * Generator testcase for the gradingforum_guide generator. * * @package gradingform_guide * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class generator_test extends \advanced_testcase { /** * Test guide creation. */ public function test_guide_creation(): void { global $DB; $this->resetAfterTest(true); // Fetch generators. $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); // Create items required for testing. $course = $generator->create_course(); $module = $generator->create_module('assign', ['course' => $course]); $user = $generator->create_user(); $context = context_module::instance($module->cmid); // Data for testing. $name = 'myfirstguide'; $description = 'My first guide'; $criteria = [ 'Alphabet' => [ 'description' => 'How well you know your alphabet', 'descriptionmarkers' => 'Basic literacy: Alphabet', 'maxscore' => 5, ], 'Times tables' => [ 'description' => 'How well you know your times-tables', 'descriptionmarkers' => 'Basic numeracy: Multiplication', 'maxscore' => 10, ], ]; // Unit under test. $this->setUser($user); $controller = $guidegenerator->create_instance($context, 'mod_assign', 'submission', $name, $description, $criteria); $this->assertInstanceOf(gradingform_guide_controller::class, $controller); $definition = $controller->get_definition(); $this->assertEquals('guide', $definition->method); $this->assertNotEmpty($definition->id); $this->assertEquals($name, $definition->name); $this->assertEquals($description, $definition->description); $this->assertEquals(gradingform_controller::DEFINITION_STATUS_READY, $definition->status); $this->assertNotEmpty($definition->timecreated); $this->assertNotEmpty($definition->timemodified); $this->assertEquals($user->id, $definition->usercreated); $this->assertNotEmpty($definition->guide_criteria); $this->assertCount(2, $definition->guide_criteria); // Check the alphabet criteria. $criteriaids = array_keys($definition->guide_criteria); $alphabet = $definition->guide_criteria[$criteriaids[0]]; $this->assertNotEmpty($alphabet['id']); $this->assertEquals(1, $alphabet['sortorder']); $this->assertEquals('How well you know your alphabet', $alphabet['description']); $this->assertEquals('Basic literacy: Alphabet', $alphabet['descriptionmarkers']); $this->assertEquals(5, $alphabet['maxscore']); // Check the times tables criteria. $tables = $definition->guide_criteria[$criteriaids[1]]; $this->assertNotEmpty($tables['id']); $this->assertEquals(2, $tables['sortorder']); $this->assertEquals('How well you know your times-tables', $tables['description']); $this->assertEquals('Basic numeracy: Multiplication', $tables['descriptionmarkers']); $this->assertEquals(10, $tables['maxscore']); } /** * Test the get_criterion_for_values function. * This is used for finding criterion and level information within a guide. */ public function test_get_criterion_for_values(): void { global $DB; $this->resetAfterTest(true); // Fetch generators. $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); // Create items required for testing. $course = $generator->create_course(); $module = $generator->create_module('assign', ['course' => $course]); $user = $generator->create_user(); $context = context_module::instance($module->cmid); // Data for testing. $name = 'myfirstguide'; $description = 'My first guide'; $criteria = [ 'Alphabet' => [ 'description' => 'How well you know your alphabet', 'descriptionmarkers' => 'Basic literacy: Alphabet', 'maxscore' => 5, ], 'Times tables' => [ 'description' => 'How well you know your times-tables', 'descriptionmarkers' => 'Basic numeracy: Multiplication', 'maxscore' => 10, ], ]; $this->setUser($user); $controller = $guidegenerator->create_instance($context, 'mod_assign', 'submission', $name, $description, $criteria); // Valid criterion. $result = $guidegenerator->get_criterion_for_values($controller, 'Alphabet', 2); $this->assertEquals('Alphabet', $result->shortname); $this->assertEquals('How well you know your alphabet', $result->description); $this->assertEquals('Basic literacy: Alphabet', $result->descriptionmarkers); $this->assertEquals(5, $result->maxscore); // Invalid criterion. $result = $guidegenerator->get_criterion_for_values($controller, 'Foo', 0); $this->assertNull($result); } /** * Tests for the get_test_guide function. */ public function test_get_test_guide(): void { global $DB; $this->resetAfterTest(true); // Fetch generators. $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); // Create items required for testing. $course = $generator->create_course(); $module = $generator->create_module('assign', ['course' => $course]); $user = $generator->create_user(); $context = context_module::instance($module->cmid); $this->setUser($user); $guide = $guidegenerator->get_test_guide($context, 'assign', 'submissions'); $definition = $guide->get_definition(); $this->assertEquals('testguide', $definition->name); $this->assertEquals('Description text', $definition->description); $this->assertEquals(gradingform_controller::DEFINITION_STATUS_READY, $definition->status); // Should create a guide with 2 criterion. $this->assertCount(2, $definition->guide_criteria); } /** * Test the get_submitted_form_data function. */ public function test_get_submitted_form_data(): void { global $DB; $this->resetAfterTest(true); // Fetch generators. $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); // Create items required for testing. $course = $generator->create_course(); $module = $generator->create_module('assign', ['course' => $course]); $user = $generator->create_user(); $context = context_module::instance($module->cmid); $this->setUser($user); $controller = $guidegenerator->get_test_guide($context, 'assign', 'submissions'); $result = $guidegenerator->get_submitted_form_data($controller, 93, [ 'Spelling mistakes' => [ 'score' => 10, 'remark' => 'Pretty good but you had a couple of errors', ], 'Pictures' => [ 'score' => 15, 'remark' => 'Lots of nice pictures!', ] ]); $this->assertIsArray($result); $this->assertEquals(93, $result['itemid']); $this->assertIsArray($result['criteria']); $this->assertCount(2, $result['criteria']); $spelling = $guidegenerator->get_criterion_for_values($controller, 'Spelling mistakes'); $this->assertIsArray($result['criteria'][$spelling->id]); $this->assertEquals(10, $result['criteria'][$spelling->id]['score']); $this->assertEquals('Pretty good but you had a couple of errors', $result['criteria'][$spelling->id]['remark']); $pictures = $guidegenerator->get_criterion_for_values($controller, 'Pictures', 2); $this->assertIsArray($result['criteria'][$pictures->id]); $this->assertEquals(15, $result['criteria'][$pictures->id]['score']); $this->assertEquals('Lots of nice pictures!', $result['criteria'][$pictures->id]['remark']); } /** * Test the get_test_form_data function. */ public function test_get_test_form_data(): void { global $DB; $this->resetAfterTest(true); // Fetch generators. $generator = \testing_util::get_data_generator(); $guidegenerator = $generator->get_plugin_generator('gradingform_guide'); // Create items required for testing. $course = $generator->create_course(); $module = $generator->create_module('assign', ['course' => $course]); $user = $generator->create_user(); $context = context_module::instance($module->cmid); $this->setUser($user); $controller = $guidegenerator->get_test_guide($context, 'assign', 'submissions'); // Unit under test. $result = $guidegenerator->get_test_form_data( $controller, 1839, 10, 'Propper good speling', 0, 'ASCII art is not a picture' ); $this->assertIsArray($result); $this->assertEquals(1839, $result['itemid']); $this->assertIsArray($result['criteria']); $this->assertCount(2, $result['criteria']); $spelling = $guidegenerator->get_criterion_for_values($controller, 'Spelling mistakes'); $this->assertIsArray($result['criteria'][$spelling->id]); $this->assertEquals(10, $result['criteria'][$spelling->id]['score']); $this->assertEquals('Propper good speling', $result['criteria'][$spelling->id]['remark']); $pictures = $guidegenerator->get_criterion_for_values($controller, 'Pictures'); $this->assertIsArray($result['criteria'][$pictures->id]); $this->assertEquals(0, $result['criteria'][$pictures->id]['score']); $this->assertEquals('ASCII art is not a picture', $result['criteria'][$pictures->id]['remark']); } } PK'\R‚=form/guide/tests/behat/grade_listings_and_submissions.featurenu[@gradingform @gradingform_guide Feature: Verify listings and grading submissions In order to verify grade listing As a teacher I need to be able to see grades in the correct column Background: Given the following "users" exist: | username | firstname | lastname | email | | student | Student | One | one@example.com | | teacher | Teacher | One | t1@example.com | And the following "courses" exist: | shortname | fullname | | C1 | Course 1 | And the following "course enrolments" exist: | user | course | role | | student | C1 | student | | teacher | C1 | editingteacher | And the following "activities" exist: | activity | name | course | advancedgradingmethod_submissions | assignsubmission_onlinetext_enabled | | assign | Assign1 | C1 | guide | 1 | And I am on the "Course 1" course page logged in as teacher And I go to "Assign1" advanced grading definition page And I set the following fields to these values: | Name | Assign1 marking guide | | Description | Marking guide description | And I define the following marking guide: | Criterion name | Description for students | Description for markers | Maximum score | | Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 100 | And I press "Save marking guide and make it ready" And the following "mod_assign > submissions" exist: | assign | user | onlinetext | | Assign1 | student | This is a submission for assignment | @javascript Scenario: Mark and view all grades in submissions table Given I am on the "Assign1" "assign activity" page And I navigate to "Submissions" in current page administration And I click on "Grade" "link" in the ".tertiary-navigation" "css_element" And I grade by filling the marking guide with: | Criteria 1 | 50 | Excellent work! | And I press "Save changes" When I follow "View all submissions" Then "Student One" row "Grade" column of "submissions" table should contain "50.00" PK'\uދ 5form/guide/tests/behat/publish_guide_template.featurenu[@gradingform @gradingform_guide Feature: Publish guide as templates In order to save time to teachers As a manager I need to publish guides and make them available to all teachers Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | manager1 | Manager | 1 | manager1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "activities" exist: | activity | course | idnumber | name | intro | advancedgradingmethod_submissions | | assign | C1 | A1 | Test assignment 1 name | TA1 | guide | | assign | C1 | A2 | Test assignment 2 name | TA2 | guide | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "system role assigns" exist: | user | role | contextlevel | reference | | manager1 | manager | System | | And I log in as "manager1" And I am on "Course 1" course homepage And I go to "Test assignment 1 name" advanced grading definition page And I set the following fields to these values: | Name | Assignment 1 marking guide | | Description | Marking guide test description | And I define the following marking guide: | Criterion name | Description for students | Description for markers | Maximum score | | Guide criterion A | Guide A description for students | Guide A description for markers | 40 | | Guide criterion B | Guide B description for students | Guide B description for markers | 60 | And I define the following frequently used comments: | Comment 1 | And I press "Save marking guide and make it ready" And I publish "Test assignment 1 name" grading form definition as a public template And I log out Scenario: Pick grading form from public template When I log in as "teacher1" And I am on "Course 1" course homepage And I go to "Test assignment 2 name" advanced grading page And I set "Test assignment 2 name" activity to use "Assignment 1 marking guide" grading form Then I should see "Ready for use" And I should see "Assignment 1 marking guide" And I should see "Marking guide test description" And I should see "Guide criterion A" And I should see "Guide criterion B" And I should see "Comment 1" PK'\4)form/guide/tests/behat/edit_guide.featurenu[@gradingform @gradingform_guide Feature: Marking guides can be created and edited In order to use and refine marking guide to grade students As a teacher I need to edit previously used marking guides Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | activity | assign | | course | C1 | | idnumber | assign1 | | name | Test assignment 1 name | | intro | Test assignment description | | section | 1 | | assignsubmission_file_enabled | 1 | | assignsubmission_onlinetext_enabled | 1 | | assignsubmission_file_maxfiles | 1 | | assignsubmission_file_maxsizebytes | 1000 | | assignfeedback_comments_enabled | 1 | | assignfeedback_file_enabled | 1 | | assignfeedback_comments_commentinline | 1 | And I am on the "Test assignment 1 name" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | Grading method | Marking guide | And I press "Save and return to course" # Defining a marking guide When I go to "Test assignment 1 name" advanced grading definition page And I change window size to "large" And I set the following fields to these values: | Name | Assignment 1 marking guide | | Description | Marking guide test description | And I define the following marking guide: | Criterion name | Description for students | Description for markers | Maximum score | | Guide criterion A | Guide A description for students | Guide A description for markers | 30 | | Guide criterion B | Guide B description for students | Guide B description for markers | 30 | | Guide criterion C | Guide C description for students | Guide C description for markers | 40 | And I define the following frequently used comments: | Comment 1 | | Comment 2 | | Comment 3 | | Comment "4" | And I press "Save marking guide and make it ready" Then I should see "Ready for use" And I should see "Guide criterion A" And I should see "Guide criterion B" And I should see "Guide criterion C" And I should see "Comment 1" And I should see "Comment 2" And I should see "Comment 3" And I should see "Comment \"4\"" @javascript Scenario: Deleting criterion and comment # Deleting criterion When I am on "Course 1" course homepage And I go to "Test assignment 1 name" advanced grading definition page And I click on "Delete criterion" "button" in the "Guide criterion B" "table_row" And I press "Yes" And I press "Save" Then I should see "Guide criterion A" And I should see "Guide criterion C" And I should see "WARNING: Your marking guide has a maximum grade of 70 points" But I should not see "Guide criterion B" # Deleting a frequently used comment When I am on "Course 1" course homepage And I go to "Test assignment 1 name" advanced grading definition page And I click on "Delete comment" "button" in the "Comment 3" "table_row" And I press "Yes" And I press "Save" Then I should see "Comment 1" And I should see "Comment 2" And I should see "Comment \"4\"" But I should not see "Comment 3" @javascript Scenario: Grading and viewing graded marking guide # Grading a student. When I navigate to "Assignment" in current page administration And I go to "Student 1" "Test assignment 1 name" activity advanced grading page And I grade by filling the marking guide with: | Guide criterion A | 25 | Very good | | Guide criterion B | 20 | | | Guide criterion C | 35 | Nice! | # Inserting frequently used comment. And I click on "Insert frequently used comment" "button" in the "Guide criterion B" "table_row" And I wait "1" seconds And I press "Comment \"4\"" And I wait "1" seconds Then the field "Guide criterion B criterion remark" matches value "Comment \"4\"" When I press "Save changes" And I am on the "Test assignment 1 name" "assign activity" page And I navigate to "Submissions" in current page administration # Checking that the user grade is correct. Then I should see "80" in the "Student 1" "table_row" And I log out # Viewing it as a student. And I am on the "Test assignment 1 name" "assign activity" page logged in as student1 And I should see "80" in the ".feedback" "css_element" And I should see "Marking guide test description" in the ".feedback" "css_element" And I should see "Very good" And I should see "Comment \"4\"" And I should see "Nice!" Scenario: I can use marking guides to grade and edit them later updating students grades with Javascript disabled PK'\~Z,Z,2form/guide/tests/behat/behat_gradingform_guide.phpnu[. /** * Steps definitions for marking guides. * * @package gradingform_guide * @category test * @copyright 2015 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php'); use Behat\Gherkin\Node\TableNode as TableNode, Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException, Behat\Mink\Exception\ExpectationException as ExpectationException; /** * Steps definitions to help with marking guides. * * @package gradingform_guide * @category test * @copyright 2015 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_gradingform_guide extends behat_base { /** * Defines the marking guide with the provided data, following marking guide's definition grid cells. * * This method fills the marking guide of the marking guide definition * form; the provided TableNode should contain one row for * each criterion and each cell of the row should contain: * # Criterion name, a.k.a. shortname * # Description for students * # Description for markers * # Max score * * Works with both JS and non-JS. * * @When /^I define the following marking guide:$/ * @throws ExpectationException * @param TableNode $guide */ public function i_define_the_following_marking_guide(TableNode $guide) { $steptableinfo = '| Criterion name | Description for students | Description for markers | Maximum score |'; if ($criteria = $guide->getHash()) { $addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_guide')); foreach ($criteria as $index => $criterion) { // Make sure the criterion array has 4 elements. if (count($criterion) != 4) { throw new ExpectationException( 'The criterion definition should contain name, description for students and markers, and maximum points. ' . 'Please follow this format: ' . $steptableinfo, $this->getSession() ); } // On load, there's already a criterion template ready. $shortnamevisible = false; if ($index > 0) { // So if the index is greater than 0, we click the Add new criterion button to add a new criterion. $addcriterionbutton->click(); $shortnamevisible = true; } $criterionroot = 'guide[criteria][NEWID' . ($index + 1) . ']'; // Set the field value for the Criterion name. $this->set_guide_field_value($criterionroot . '[shortname]', $criterion['Criterion name'], $shortnamevisible); // Set the field value for the Description for students field. $this->set_guide_field_value($criterionroot . '[description]', $criterion['Description for students']); // Set the field value for the Description for markers field. $this->set_guide_field_value($criterionroot . '[descriptionmarkers]', $criterion['Description for markers']); // Set the field value for the Max score field. $this->set_guide_field_value($criterionroot . '[maxscore]', $criterion['Maximum score']); } } } /** * Edits an existing marking guide with the provided data. * * This method edits the marking guide of the marking guide definition * form; the provided TableNode should contain one row for * each field and each cell of the row should contain: * | Field name | New value | * | shortname | Updated Grade criterion | * * @When /^I edit the marking guide criterion "([^"]*)" with the following values:$/ * @param string $criterionname * @param TableNode $fields */ public function i_edit_the_marking_guide_criterion_with_the_following_values(string $criterionname, TableNode $fields) { if ($fieldvalues = $fields->getHash()) { $criterionid = 0; $locator = "//tr[contains(@class, 'criterion')]//div[@class='criterionname']" . "//span[@class='textvalue'][text()='$criterionname']/ancestor::tr"; if ($criterionrow = $this->find('xpath', $locator)) { $criterionid = str_replace('guide-criteria-', '', $criterionrow->getAttribute('id')); } if ($criterionid) { $criterionroot = 'guide[criteria]' . '[' . $criterionid . ']'; foreach ($fieldvalues as $fieldvalue) { // Make sure the fieldvalue array has 2 elements. if (count($fieldvalue) != 2) { throw new ExpectationException( 'The field definition should contain field name and new value. ' . 'Please follow this format: | Field name | New value |', $this->getSession() ); } $fieldname = $fieldvalue['Field name']; $newvalue = $fieldvalue['New value']; $this->set_guide_field_value($criterionroot . "[$fieldname]", $newvalue); } } else { throw new ExpectationException( 'Criterion with name "' . $criterionname . '" not found.', $this->getSession() ); } } } /** * Defines the marking guide with the provided data, following marking guide's definition grid cells. * * This method fills the table of frequently used comments of the marking guide definition form. * The provided TableNode should contain one row for each frequently used comment. * Each row contains: * # Comment * * Works with both JS and non-JS. * * @When /^I define the following frequently used comments:$/ * @throws ExpectationException * @param TableNode $commentstable */ public function i_define_the_following_frequently_used_comments(TableNode $commentstable) { $steptableinfo = '| Comment |'; if ($comments = $commentstable->getRows()) { $addcommentbutton = $this->find_button(get_string('addcomment', 'gradingform_guide')); foreach ($comments as $index => $comment) { // Make sure the comment array has only 1 element. if (count($comment) != 1) { throw new ExpectationException( 'The comment cannot be empty. Please follow this format: ' . $steptableinfo, $this->getSession() ); } // On load, there's already a comment template ready. $commentfieldvisible = false; if ($index > 0) { // So if the index is greater than 0, we click the Add frequently used comment button to add a new criterion. $addcommentbutton->click(); $commentfieldvisible = true; } $commentroot = 'guide[comments][NEWID' . ($index + 1) . ']'; // Set the field value for the frequently used comment. $this->set_guide_field_value($commentroot . '[description]', $comment[0], $commentfieldvisible); } } } /** * Performs grading of the student by filling out the marking guide. * Set one line per criterion and for each criterion set "| Criterion name | Points | Remark |". * * @When /^I grade by filling the marking guide with:$/ * * @throws ExpectationException * @param TableNode $guide * @return void */ public function i_grade_by_filling_the_marking_guide_with(TableNode $guide) { $criteria = $guide->getRowsHash(); $stepusage = '"I grade by filling the rubric with:" step needs you to provide a table where each row is a criterion' . ' and each criterion has 3 different values: | Criterion name | Number of points | Remark text |'; // First element -> name, second -> points, third -> Remark. foreach ($criteria as $name => $criterion) { // We only expect the points and the remark, as the criterion name is $name. if (count($criterion) !== 2) { throw new ExpectationException($stepusage, $this->getSession()); } // Numeric value here. $points = $criterion[0]; if (!is_numeric($points)) { throw new ExpectationException($stepusage, $this->getSession()); } $criterionid = 0; if ($criterionnamediv = $this->find('xpath', "//div[@class='criterionshortname'][text()='$name']")) { $criteriondivname = $criterionnamediv->getAttribute('name'); // Criterion's name is of the format "advancedgrading[criteria][ID][shortname]". // So just explode the string with "][" as delimiter to extract the criterion ID. if ($nameparts = explode('][', $criteriondivname)) { $criterionid = $nameparts[1]; } } if ($criterionid) { $criterionroot = 'advancedgrading[criteria]' . '[' . $criterionid . ']'; $this->execute('behat_forms::i_set_the_field_to', array($criterionroot . '[score]', $points)); $this->execute('behat_forms::i_set_the_field_to', array($criterionroot . '[remark]', $criterion[1])); } } } /** * Makes a hidden marking guide field visible (if necessary) and sets a value on it. * * @param string $name The name of the field * @param string $value The value to set * @param bool $visible * @return void */ protected function set_guide_field_value($name, $value, $visible = false) { // Fields are hidden by default. if ($this->running_javascript() && $visible === false) { $xpath = "//*[@name='$name']/following-sibling::*[contains(concat(' ', normalize-space(@class), ' '), ' plainvalue ')]"; $textnode = $this->find('xpath', $xpath); $textnode->click(); } // Set the value now. $field = $this->find_field($name); $field->setValue($value); } } PK'\w;form/guide/tests/behat/edit_marking_guide_criterion.featurenu[@gradingform @gradingform_guide Feature: Marking guide criterion can be edited and replaced In order to edit and replace a marking guide criterion As a teacher I need to have an existing marking guide criterion Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | name | advancedgradingmethod_submissions | assignsubmission_onlinetext_enabled | | assign | C1 | Assign 1 | guide | 1 | And I am on the "Course 1" course page logged in as teacher1 And I go to "Assign 1" advanced grading definition page And I set the following fields to these values: | Name | Assign 1 marking guide | | Description | Marking guide description | And I define the following marking guide: | Criterion name | Description for students | Description for markers | Maximum score | | Grade Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 70 | | Grade Criteria 2 | Grade 2 description for students | Grade 2 description for markers | 30 | And I define the following frequently used comments: | Comment 1 | | Comment 2 | | Comment 3 | And I press "Save marking guide and make it ready" And the following "mod_assign > submissions" exist: | assign | user | onlinetext | | Assign 1 | student1 | I'm the student first submission | And I am on the "Assign 1" "assign activity" page And I go to "Student 1" "Assign 1" activity advanced grading page And I grade by filling the marking guide with: | Grade Criteria 1 | 50 | Comment 1 | | Grade Criteria 2 | 20 | Comment 2 | And I press "Save changes" @javascript Scenario: Marking guide frequently used comments can be updated and reordered Given I go to "Assign 1" advanced grading definition page And I click on "Move up" "button" in the "Comment 3" "table_row" And I click on "Move down" "button" in the "Comment 1" "table_row" When I press "Save" # Confirm that the order of the frequently used comments are updated. Then "Comment 3" "text" should appear before "Comment 1" "text" And "Comment 2" "text" should appear after "Comment 1" "text" And I am on the "Assign 1" "assign activity" page And I go to "Student 1" "Assign 1" activity advanced grading page # Confirm that there are no changes to the existing marking records. And I should see "Comment 1" in the "Grade Criteria 1" "table_row" And I should see "Comment 2" in the "Grade Criteria 2" "table_row" @javascript Scenario: Marking guide criterion can be modified Given I go to "Assign 1" advanced grading definition page And I click on "Delete criterion" "button" in the "Grade Criteria 1" "table_row" And I press "Yes" And I wait "1" seconds And I should not see "Grade Criteria 1" And I press "Add criterion" # Used xpath_element selectors in order to differentiate multiple criterion fields. And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criterionname']" "xpath_element" # Set the new criterion name. And I set the field with xpath "//input[@id='guide-criteria-NEWID1-shortname']" to "Grade Criteria 3" And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criteriondesc']//span[@class='textvalue']" "xpath_element" # Set the new criterion description for students. And I set the field with xpath "//textarea[@id='guide[criteria][NEWID1][description]']" to "Grade 3 description for students" And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criteriondescmarkers']//span[@class='textvalue']" "xpath_element" # Set the new criterion description for markers. And I set the field with xpath "//textarea[@id='guide[criteria][NEWID1][descriptionmarkers]']" to "Grade 3 description for markers" And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criterionmaxscore']//span[@class='textvalue']" "xpath_element" # Set the new criterion max score. And I set the field with xpath "//input[@id='guide[criteria][NEWID1][maxscore]']" to "70" When I press "Save" Then "You are about to save significant changes to a marking guide that has already been used for grading. The gradebook value will be unchanged, but the marking guide will be hidden from students until their item is regraded." "text" should exist And I press "Continue" And I am on the "Assign 1" "assign activity" page logged in as student1 # Confirm that the remarks for each grading criterion are not displayed. And "Comment 1" "text" should not exist And "Comment 2" "text" should not exist # Confirm that the submission is not regraded. And I should see "70.00 / 100.00" # Regrade the submission. And I am on the "Course 1" course page logged in as teacher1 And I go to "Student 1" "Assign 1" activity advanced grading page And I grade by filling the marking guide with: | Grade Criteria 2 | 15 | Comment 3 | | Grade Criteria 3 | 50 | Comment 2 | And I press "Save changes" And I am on the "Assign 1" "assign activity" page logged in as student1 # Confirm that submission is re-marked and regraded. And I should see "65.00 / 100.00" And I should see the marking guide information displayed as: | criteria | description | remark | maxscore | criteriascore | | Grade Criteria 2 | Grade 2 description for students | Comment 3 | 30 | 15 / 30 | | Grade Criteria 3 | Grade 3 description for students | Comment 2 | 70 | 50 / 70 | PK'\JƄ 7form/guide/tests/behat/guide_max_grade_mismatch.featurenu[@gradingform @gradingform_guide Feature: Marking guide can handle maximum grade mismatches In order to handle maximum grade mismatches As a teacher I should be able to set the maximum grade Background: Given the following "user" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | Scenario Outline: Marking guide maximum grade handling Given the following "activities" exist: | activity | course | name | advancedgradingmethod_submissions | | assign | C1 | Assign 1 | guide | And I am on the "Course 1" course page logged in as teacher1 And I go to "Assign 1" advanced grading definition page And I set the following fields to these values: | Name | Assign 1 marking guide | | Description | Marking guide description | And I define the following marking guide: | Criterion name | Description for students | Description for markers | Maximum score | | Grade Criteria 1 | Grade 1 description for students | Grade 1 description for markers | | | Grade Criteria 2 | Grade 2 description for students | Grade 2 description for markers | 30 | When I press "Save marking guide and make it ready" Then I should see "Assign 1 marking guide Ready for use" # Please note: We need to add the no-break space unicode character to the warning message otherwise it will fail. And I should see "WARNING: Your marking guide has a maximum grade of points but the maximum grade set in your activity is 100 The maximum score set in your marking guide will be scaled to the maximum grade in the module." And I should see "Intermediate scores will be converted respectively and rounded to the nearest available grade." Examples: # value is derived from + maximum score assigned to criteria 2 (30). # Case 1: total > max score of 100. # Case 2: total < max score of 100. | maxscore | totalmaxscore | | 90 | 120 | | 50 | 80 | PK'\O3form/guide/tests/behat/assign_marking_guide.featurenu[@gradingform @gradingform_guide @javascript Feature: Display marking guide information to students In order for students to see the marking guide information As a teacher I should be able to change display settings for marking guide information Background: Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | 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 "activities" exist: | activity | course | name | advancedgradingmethod_submissions | | assign | C1 | Assign 1 | guide | And I am on the "Course 1" course page logged in as teacher1 And I go to "Assign 1" advanced grading definition page And I set the following fields to these values: | Name | Assign 1 marking guide | | Description | Marking guide description | And I define the following marking guide: | Criterion name | Description for students | Description for markers | Maximum score | | Grade Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 70 | | Grade Criteria 2 | Grade 2 description for students | Grade 2 description for markers | 30 | And I press "Save marking guide and make it ready" Scenario: Confirm that marking guide information is not displayed after student is graded # Update the existing marking guide to ensure that marks per criterion is displayed. Given I click on "Edit the current form definition" "link" And I set the field "Show marks per criterion to students" to "0" And I press "Save" And I am on the "Assign 1" "assign activity" page And I go to "Student 1" "Assign 1" activity advanced grading page And I grade by filling the marking guide with: | Grade Criteria 1 | 50 | Excellent work! | | Grade Criteria 2 | 20 | Try harder | And I press "Save changes" When I am on the "Assign 1" "assign activity" page logged in as student1 # Confirm the marking guide information display after student is graded when marking per criterion display is disabled. # Confirm that overall grade is displayed. Then I should see "70.00 / 100.00" And I should see the marking guide information displayed as: | criteria | description | remark | | Grade Criteria 1 | Grade 1 description for students | Excellent work! | | Grade Criteria 2 | Grade 2 description for students | Try harder | Scenario: Confirm that marking guide information is displayed after student is graded Given I am on the "Assign 1" "assign activity" page logged in as student1 And I should see "Grade 1 description for students" in the "Grade Criteria 1" "table_row" And I should see "Grade 2 description for students" in the "Grade Criteria 2" "table_row" # No grade to student1 yet. And I should not see "70.00 / 100.00" # No need to update marking guide as marking guide definition is already enabled by default And I am on the "Assign 1" "assign activity" page logged in as teacher1 And I go to "Student 1" "Assign 1" activity advanced grading page And I grade by filling the marking guide with: | Grade Criteria 1 | 50 | Excellent work! | | Grade Criteria 2 | 20 | Try harder | And I press "Save changes" When I am on the "Assign 1" "assign activity" page logged in as student1 # Student1 grade is now displayed. Then I should see "70.00 / 100.00" And I should see the marking guide information displayed as: | criteria | description | remark | maxscore | criteriascore | | Grade Criteria 1 | Grade 1 description for students | Excellent work! | 70 | 50 / 70 | | Grade Criteria 2 | Grade 2 description for students | Try harder | 30 | 20 / 30 | Scenario: Confirm that marking guide definition is retained when grading method is changed Given I am on the "Assign 1" "assign activity" page And I go to "Student 1" "Assign 1" activity advanced grading page And I grade by filling the marking guide with: | Grade Criteria 1 | 70 | Well done! | | Grade Criteria 2 | 20 | Great work | And I press "Save changes" And I am on the "Assign 1" "assign activity editing" page And I set the following fields to these values: | Grading method | Simple direct grading | And I press "Save and return to course" When I go to "Assign 1" advanced grading page Then I should not see "Assign 1 marking guide Ready for use" And I should not see "Grade Critera 1" And I should not see "Grade Critera 2" And I am on the "Course 1" "grades > Grader report > View" page And the following should exist in the "user-grades" table: | -1- | -2- | -3- | | Student 1 | student1@example.com | 90 | And I am on the "Assign 1" "assign activity editing" page And I set the following fields to these values: | Grading method | Marking guide | And I press "Save and return to course" And I go to "Assign 1" advanced grading page And I should see "Assign 1 marking guide Ready for use" And I should see "Grade Criteria 1" And I should see "Grade Criteria 2" PK'\y8form/guide/tests/behat/edit_marking_guide_detail.featurenu[@gradingform @gradingform_guide Feature: Editing a marking guide already used for grading updates regrade state and student visible grades In order to update marking guide details As a teacher I need to be able to grade a submission Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | One | teacher1@example.com | | student1 | Student | One | student1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | name | advancedgradingmethod_submissions | assignsubmission_onlinetext_enabled | | assign | C1 | Assign 1 | guide | 1 | And the following "mod_assign > submissions" exist: | assign | user | onlinetext | | Assign 1 | student1 | Assign 1 submission | And I am on the "Course 1" course page logged in as teacher1 And I go to "Assign 1" advanced grading definition page And I set the following fields to these values: | Name | Assign 1 marking guide | | Description | Marking guide description | And I define the following marking guide: | Criterion name | Description for students | Description for markers | Maximum score | | Grade criterion A | Grade 1 description for students | Grade 1 description for markers | 70 | | Grade criterion B | Grade 2 description for students | Grade 2 description for markers | 30 | And I press "Save marking guide and make it ready" And I navigate to "Assignment" in current page administration And I go to "Student One" "Assign 1" activity advanced grading page And I grade by filling the marking guide with: | Grade criterion A | 25 | Needs improvement | | Grade criterion B | 20 | Excellent! | And I press "Save changes" And I am on "Course 1" course homepage And I go to "Assign 1" advanced grading definition page And I edit the marking guide criterion "Grade criterion A" with the following values: | Field name | New value | | shortname | Updated Grade criterion A | | description | Updated description for students | | descriptionmarkers | Updated description for markers | | maxscore | 60 | And I press "Save" @javascript Scenario: Teacher edits a used marking guide and mark it for regrade and student sees breakdown only after teacher regrades # Set the marking guide to be "Mark for regrade". Given I set the field "menuguideregrade" to "Mark for regrade" And I should see "You are about to save changes to a marking guide that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the marking guide will be hidden from students until their item is regraded." And I click on "Continue" "button" And I am on the "Assign 1" "assign activity" page logged in as student1 # Student should not see the grade breakdown as the activity is marked for regrade. And I should not see "Grade breakdown" When I am on the "Assign 1" "assign activity" page logged in as teacher1 And I go to "Student One" "Assign 1" activity advanced grading page And I grade by filling the marking guide with: | Updated Grade criterion A | 50 | I changed my mind | | Grade criterion B | 30 | It's all good now | And I press "Save changes" And I am on the "Assign 1" "assign activity" page logged in as student1 # Now the student should see the updated grade breakdown. Then I should see "Grade breakdown" And I should see the marking guide information displayed as: | criteria | description | remark | maxscore | criteriascore | | Updated Grade criterion A | Updated description for students | I changed my mind | 60 | 50 / 60 | | Grade criterion B | Grade 2 description for students | It's all good now | 30 | 30 / 30 | @javascript Scenario: Teacher edits a used marking guide does not mark for regrade existing student grades remain visible and unchanged # Set the marking guide to be "Do not mark for regrade". Given I set the field "menuguideregrade" to "Do not mark for regrade" And I should see "You are about to save changes to a marking guide that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the marking guide will be hidden from students until their item is regraded." And I click on "Continue" "button" When I am on the "Assign 1" "assign activity" page logged in as student1 # Student should see the updated grade breakdown as the activity was not marked for regrading. Then I should see "Grade breakdown" And I should see the marking guide information displayed as: | criteria | description | remark | maxscore | criteriascore | | Updated Grade criterion A | Updated description for students | Needs improvement | 60 | 25 / 60 | | Grade criterion B | Grade 2 description for students | Excellent! | 30 | 20 / 30 | PK'\*yޭ7form/guide/tests/behat/edit_marking_guide_state.featurenu[@gradingform @gradingform_guide Feature: Teacher can edit a marking guide state In order to change marking guide back to draft As a teacher I need to be able to edit the marking guide status Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | course | name | advancedgradingmethod_submissions | | assign | C1 | Assign 1 | guide | Scenario Outline: Marking guide state can be changed Given I am on the "Course 1" course page logged in as teacher1 And I go to "Assign 1" advanced grading definition page And I set the following fields to these values: | Name | Assign 1 marking guide | | Description | Marking guide description | And I define the following marking guide: | Criterion name | Description for students | Description for markers | Maximum score | | Grade Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 70 | | Grade Criteria 2 | Grade 2 description for students | Grade 2 description for markers | 30 | When I press "" Then I see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status." And I see "Assign 1 marking guide Draft" And I see "Assign 1 marking guide Ready for use" Examples: | buttonname | drafttextvisibility | readytextvisibility | | Save marking guide and make it ready | should not | should | | Save as draft | should | should not | PK'\#ve3form/guide/tests/behat/define_marking_guide.featurenu[@gradingform @gradingform_guide Feature: Teacher can define a marking guide As a teacher, I should be able to define a marking guide Background: Given the following "users" exist: | username | firtname | lastname | email | | teacher1 | Teacher | One | teacher1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | course | name | advancedgradingmethod_submissions | | assign | C1 | Assign 1 | guide | And I am on the "Course 1" course page logged in as teacher1 And I go to "Assign 1" advanced grading definition page And I set the following fields to these values: | Name | Marking guide 1 | Scenario: No criterion added to marking guide When I press "Save as draft" # Confirm that criterion parameters are required Then I should see "Criterion name can not be empty" And I should see "Criterion max score can not be empty" # Confirm that marking guide is not saved due to the missing criterion And I should not see "Marking guide 1 Draft" And I should not see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status." @javascript Scenario: Marking guide criterion is added to marking guide Given I define the following marking guide: | Criterion name | Description for students | Description for markers | Maximum score | | Criteria 1 | Criteria 1 description for student | Criteria 1 description for marker | 70 | | Criteria 2 | Criteria 2 description for student | Criteria 2 description for marker | 30 | # Move Criteria 1 below Criteria 2 And I click on "Move down" "button" in the "Criteria 1" "table_row" When I press "Save as draft" And I go to "Assign 1" advanced grading definition page # Confirm that the order of criterion shown matches input -- Criteria 2 is listed before Criteria 1 Then "Move down" "button" in the "Criteria 2" "table_row" should be visible And "Move up" "button" in the "Criteria 2" "table_row" should not be visible And "Move up" "button" in the "Criteria 1" "table_row" should be visible And "Move down" "button" in the "Criteria 1" "table_row" should not be visible # Confirm the other information entered were saved And I should see "Criteria 2 description for student" in the "Criteria 2" "table_row" And I should see "Criteria 2 description for marker" in the "Criteria 2" "table_row" And I should see "30" in the "Criteria 2" "table_row" And I should see "Criteria 1 description for student" in the "Criteria 1" "table_row" And I should see "Criteria 1 description for marker" in the "Criteria 1" "table_row" And I should see "70" in the "Criteria 1" "table_row" Scenario: Marking guide options and frequently used comment are added to marking guide Given I define the following marking guide: | Criterion name | Description for students | Description for markers | Maximum score | | Criteria 1 | Criteria 1 description for student | Criteria 1 description for marker | 50 | | Criteria 2 | Criteria 2 description for student | Criteria 2 description for marker | 50 | # Add frequently used comments and other marking guide options And I define the following frequently used comments: | Comment 1 | | Comment 2 | And I set the following fields to these values: | Show guide definition to students | 1 | | Show marks per criterion to students | 0 | When I press "Save as draft" And I go to "Assign 1" advanced grading definition page # Confirm that frequently used comments and marking guide options specified during registration are retained Then I should see "Comment 1" And I should see "Comment 2" And the field "Show guide definition to students" matches value "1" And the field "Show marks per criterion to students" matches value "0" PK'\BcWy  3form/guide/tests/behat/delete_marking_guide.featurenu[@gradingform @gradingform_guide Feature: Teacher can delete marking guide As a teacher, I should be able to delete a marking guide Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | One | teacher1@example.com | | student1 | Student | One | student1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | name | advancedgradingmethod_submissions | | assign | C1 | Assign 1 | guide | And I am on the "Course 1" course page logged in as teacher1 And I go to "Assign 1" advanced grading definition page And I set the following fields to these values: | Name | Marking guide 1 | And I define the following marking guide: | Criterion name | Description for students | Description for markers | Maximum score | | Criterion 1 | Criterion 1 description for student | Criterion 1 description for markers | 100 | And I press "Save marking guide and make it ready" @javascript Scenario: Delete a marking guide Given I am on the "Assign 1" "assign activity" page And I go to "Student One" "Assign 1" activity advanced grading page And I grade by filling the marking guide with: | Criterion 1 | 70 | Well done! | And I press "Save changes" And I go to "Assign 1" advanced grading page When I click on "Delete the currently defined form" "link" Then I should see "You are going to delete the grading form 'Marking guide 1' and all the associated information from 'Assign 1 (Submissions)'" And I press "Cancel" # Confirm that marking guide was not deleted if Cancel is pressed And I should see "Marking guide 1 Ready for use" And I should see "Criterion 1" And I click on "Delete the currently defined form" "link" And I press "Continue" # Confirm that marking guide was deleted successfully if Continue is pressed And I should see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status." And I should not see "Marking guide 1 Ready for use" And I should not see "Criterion 1" And I am on the "Course 1" "grades > Grader report > View" page And the following should exist in the "user-grades" table: | -1- | -2- | -3- | | Student One | student1@example.com | 70 | PK'\mw]:]:form/guide/guideeditor.phpnu[. /** * This file contains the marking guide editor element * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once("HTML/QuickForm/input.php"); /** * The editor for the marking guide advanced grading plugin. * * @package gradingform_guide * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class moodlequickform_guideeditor extends HTML_QuickForm_input { /** @var string help message */ public $_helpbutton = ''; /** @var null|false|string stores the result of the last validation: null - undefined, false - no errors, * string - error(s) text */ protected $validationerrors = null; /** @var bool if element has already been validated **/ protected $wasvalidated = false; /** @var null|bool If non-submit (JS) button was pressed: null - unknown, true/false - button was/wasn't pressed */ protected $nonjsbuttonpressed = false; /** @var string|false Message to display in front of the editor (that there exist grades on this guide being edited) */ protected $regradeconfirmation = false; /** * Constructor * * @param string $elementname * @param string $elementlabel * @param array $attributes */ public function __construct($elementname=null, $elementlabel=null, $attributes=null) { parent::__construct($elementname, $elementlabel, $attributes); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function moodlequickform_guideeditor($elementname=null, $elementlabel=null, $attributes=null) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($elementname, $elementlabel, $attributes); } /** * get html for help button * * @return string html for help button */ public function getHelpButton() { return $this->_helpbutton; } /** * The renderer will take care itself about different display in normal and frozen states * * @return string */ public function getElementTemplateType() { return 'default'; } /** * Specifies that confirmation about re-grading needs to be added to this rubric editor. * $changelevel is saved in $this->regradeconfirmation and retrieved in toHtml() * * @see gradingform_rubric_controller::update_or_check_rubric() * @param int $changelevel */ public function add_regrade_confirmation($changelevel) { $this->regradeconfirmation = $changelevel; } /** * Returns html string to display this element * * @return string */ public function toHtml() { global $PAGE; $html = $this->_getTabs(); $renderer = $PAGE->get_renderer('gradingform_guide'); $data = $this->prepare_data(null, $this->wasvalidated); if (!$this->_flagFrozen) { $mode = gradingform_guide_controller::DISPLAY_EDIT_FULL; $module = array('name'=>'gradingform_guideeditor', 'fullpath'=>'/grade/grading/form/guide/js/guideeditor.js', 'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'), 'strings' => array( array('confirmdeletecriterion', 'gradingform_guide'), array('clicktoedit', 'gradingform_guide'), array('clicktoeditname', 'gradingform_guide') )); $PAGE->requires->js_init_call('M.gradingform_guideeditor.init', array( array('name' => $this->getName(), 'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()), 'commenttemplate' => $renderer->comment_template($mode, $this->getName()) )), true, $module); } else { // Guide is frozen, no javascript needed. if ($this->_persistantFreeze) { $mode = gradingform_guide_controller::DISPLAY_EDIT_FROZEN; } else { $mode = gradingform_guide_controller::DISPLAY_PREVIEW; } } if ($this->regradeconfirmation) { if (!isset($data['regrade'])) { $data['regrade'] = 1; } $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']); } if ($this->validationerrors) { $html .= html_writer::div($renderer->notification($this->validationerrors)); } $html .= $renderer->display_guide($data['criteria'], $data['comments'], $data['options'], $mode, $this->getName()); return $html; } /** * Prepares the data passed in $_POST: * - processes the pressed buttons 'addlevel', 'addcriterion', 'moveup', 'movedown', 'delete' (when JavaScript is disabled) * sets $this->nonjsbuttonpressed to true/false if such button was pressed * - if options not passed (i.e. we create a new guide) fills the options array with the default values * - if options are passed completes the options array with unchecked checkboxes * - if $withvalidation is set, adds 'error_xxx' attributes to elements that contain errors and creates an error string * and stores it in $this->validationerrors * * @param array $value * @param boolean $withvalidation whether to enable data validation * @return array */ protected function prepare_data($value = null, $withvalidation = false) { if (null === $value) { $value = $this->getValue(); } if ($this->nonjsbuttonpressed === null) { $this->nonjsbuttonpressed = false; } $errors = array(); $return = array('criteria' => array(), 'options' => gradingform_guide_controller::get_default_options(), 'comments' => array()); if (!isset($value['criteria'])) { $value['criteria'] = array(); $errors['err_nocriteria'] = 1; } // If options are present in $value, replace default values with submitted values. if (!empty($value['options'])) { foreach (array_keys($return['options']) as $option) { // Special treatment for checkboxes. if (!empty($value['options'][$option])) { $return['options'][$option] = $value['options'][$option]; } else { $return['options'][$option] = null; } } } if (is_array($value)) { // For other array keys of $value no special treatmeant neeeded, copy them to return value as is. foreach (array_keys($value) as $key) { if ($key != 'options' && $key != 'criteria' && $key != 'comments') { $return[$key] = $value[$key]; } } } // Iterate through criteria. $lastaction = null; $lastid = null; foreach ($value['criteria'] as $id => $criterion) { if ($id == 'addcriterion') { $id = $this->get_next_id(array_keys($value['criteria'])); $criterion = array('description' => ''); $this->nonjsbuttonpressed = true; } if ($withvalidation && !array_key_exists('delete', $criterion)) { if (!strlen(trim($criterion['shortname']))) { $errors['err_noshortname'] = 1; $criterion['error_description'] = true; } if (strlen(trim($criterion['shortname'])) > 255) { $errors['err_shortnametoolong'] = 1; $criterion['error_description'] = true; } if (!strlen(trim($criterion['maxscore']))) { $errors['err_nomaxscore'] = 1; $criterion['error_description'] = true; } else if (!is_numeric($criterion['maxscore'])) { $errors['err_maxscorenotnumeric'] = 1; $criterion['error_description'] = true; } else if ($criterion['maxscore'] < 0) { $errors['err_maxscoreisnegative'] = 1; $criterion['error_description'] = true; } } if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') { unset($criterion['moveup']); if ($lastid !== null) { $lastcriterion = $return['criteria'][$lastid]; unset($return['criteria'][$lastid]); $return['criteria'][$id] = $criterion; $return['criteria'][$lastid] = $lastcriterion; } else { $return['criteria'][$id] = $criterion; } $lastaction = null; $lastid = $id; $this->nonjsbuttonpressed = true; } else if (array_key_exists('delete', $criterion)) { $this->nonjsbuttonpressed = true; } else { if (array_key_exists('movedown', $criterion)) { unset($criterion['movedown']); $lastaction = 'movedown'; $this->nonjsbuttonpressed = true; } $return['criteria'][$id] = $criterion; $lastid = $id; } } // Add sort order field to criteria. $csortorder = 1; foreach (array_keys($return['criteria']) as $id) { $return['criteria'][$id]['sortorder'] = $csortorder++; } // Iterate through comments. $lastaction = null; $lastid = null; if (!empty($value['comments'])) { foreach ($value['comments'] as $id => $comment) { if ($id == 'addcomment') { $id = $this->get_next_id(array_keys($value['comments'])); $comment = array('description' => ''); $this->nonjsbuttonpressed = true; } if (array_key_exists('moveup', $comment) || $lastaction == 'movedown') { unset($comment['moveup']); if ($lastid !== null) { $lastcomment = $return['comments'][$lastid]; unset($return['comments'][$lastid]); $return['comments'][$id] = $comment; $return['comments'][$lastid] = $lastcomment; } else { $return['comments'][$id] = $comment; } $lastaction = null; $lastid = $id; $this->nonjsbuttonpressed = true; } else if (array_key_exists('delete', $comment)) { $this->nonjsbuttonpressed = true; } else { if (array_key_exists('movedown', $comment)) { unset($comment['movedown']); $lastaction = 'movedown'; $this->nonjsbuttonpressed = true; } $return['comments'][$id] = $comment; $lastid = $id; } } // Add sort order field to comments. $csortorder = 1; foreach (array_keys($return['comments']) as $id) { $return['comments'][$id]['sortorder'] = $csortorder++; } } // Create validation error string (if needed). if ($withvalidation) { if (count($errors)) { $rv = array(); foreach ($errors as $error => $v) { $rv[] = get_string($error, 'gradingform_guide'); } $this->validationerrors = join('
', $rv); } else { $this->validationerrors = false; } $this->wasvalidated = true; } return $return; } /** * Scans array $ids to find the biggest element ! NEWID*, increments it by 1 and returns * * @param array $ids * @return string */ protected function get_next_id($ids) { $maxid = 0; foreach ($ids as $id) { if (preg_match('/^NEWID(\d+)$/', $id, $matches) && ((int)$matches[1]) > $maxid) { $maxid = (int)$matches[1]; } } return 'NEWID'.($maxid+1); } /** * Checks if a submit button was pressed which is supposed to be processed on client side by JS * but user seem to have disabled JS in the browser. * (buttons 'add criteria', 'add level', 'move up', 'move down', 'add comment') * In this case the form containing this element is prevented from being submitted * * @param array $value * @return boolean true if non-submit button was pressed and not processed by JS */ public function non_js_button_pressed($value) { if ($this->nonjsbuttonpressed === null) { $this->prepare_data($value); } return $this->nonjsbuttonpressed; } /** * Validates that guide has at least one criterion, filled definitions and all criteria * have filled descriptions * * @param array $value * @return string|false error text or false if no errors found */ public function validate($value) { if (!$this->wasvalidated) { $this->prepare_data($value, true); } return $this->validationerrors; } /** * Prepares the data for saving * @see prepare_data() * * @param array $submitvalues * @param boolean $assoc * @return array */ public function exportValue(&$submitvalues, $assoc = false) { $value = $this->prepare_data($this->_findValue($submitvalues)); return $this->_prepareValue($value, $assoc); } } PK'\`RRform/guide/styles.scssnu[.gradingform_guide-fac { position: absolute; right: -5px; top: 5px; } PK'\=s|.|.form/guide/js/guideeditor.jsnu[M.gradingform_guideeditor = {'templates' : {}, 'eventhandler' : null, 'name' : null, 'Y' : null}; /** * This function is called for each guideeditor on page. */ M.gradingform_guideeditor.init = function(Y, options) { M.gradingform_guideeditor.name = options.name M.gradingform_guideeditor.Y = Y M.gradingform_guideeditor.templates[options.name] = { 'criterion' : options.criteriontemplate, 'comment' : options.commenttemplate } M.gradingform_guideeditor.disablealleditors() Y.on('click', M.gradingform_guideeditor.clickanywhere, 'body', null) YUI().use('event-touch', function (Y) { Y.one('body').on('touchstart', M.gradingform_guideeditor.clickanywhere); Y.one('body').on('touchend', M.gradingform_guideeditor.clickanywhere); }) M.gradingform_guideeditor.addhandlers() }; // Adds handlers for clicking submit button. This function must be called each time JS adds new elements to html M.gradingform_guideeditor.addhandlers = function() { var Y = M.gradingform_guideeditor.Y var name = M.gradingform_guideeditor.name if (M.gradingform_guideeditor.eventhandler) { M.gradingform_guideeditor.eventhandler.detach() } M.gradingform_guideeditor.eventhandler = Y.on('click', M.gradingform_guideeditor.buttonclick, '#guide-'+name+' input[type=submit]', null); } // switches all input text elements to non-edit mode M.gradingform_guideeditor.disablealleditors = function() { var Y = M.gradingform_guideeditor.Y var name = M.gradingform_guideeditor.name Y.all('#guide-'+name+' .criteria .description input[type=text]:not(.pseudotablink)').each( function(node) {M.gradingform_guideeditor.editmode(node, false)} ); Y.all('#guide-'+name+' .criteria .description textarea').each( function(node) {M.gradingform_guideeditor.editmode(node, false)} ); Y.all('#guide-'+name+' .comments .description textarea').each( function(node) {M.gradingform_guideeditor.editmode(node, false)} ); } // function invoked on each click on the page. If criterion values are clicked // it switches the element to edit mode. If guide button is clicked it does nothing so the 'buttonclick' // function is invoked M.gradingform_guideeditor.clickanywhere = function(e) { if (e.type == 'touchstart') { return } var el = e.target // if clicked on button - disablecurrenteditor, continue if (el.get('tagName') == 'INPUT' && el.get('type') == 'submit') { return } // if clicked on description item and this item is not enabled - enable it var container = null if ((container = el.ancestor('.criterionname')) || (container = el.ancestor('.criterionmaxscore'))) { el = container.one('input[type=text]') } else if ((container = el.ancestor('.criteriondesc')) || (container = el.ancestor('.criteriondescmarkers'))) { el = container.one('textarea') } else { el = null } if (el) { if (el.hasClass('hiddenelement')) { M.gradingform_guideeditor.disablealleditors() M.gradingform_guideeditor.editmode(el, true) } return } // else disablecurrenteditor M.gradingform_guideeditor.disablealleditors() } // switch the criterion item to edit mode or switch back M.gradingform_guideeditor.editmode = function(el, editmode) { var Y = M.gradingform_guideeditor.Y var ta = el if (!editmode && ta.hasClass('hiddenelement')) { return; } if (editmode && !ta.hasClass('hiddenelement')) { return; } var pseudotablink = '', taplain = ta.next('.plainvalue'), tbplain = null, tb = el.one('.score input[type=text]') // add 'plainvalue' next to textarea for description/definition and next to input text field for score (if applicable) if (!taplain && ta.get('name') != '') { ta.insert('
'+pseudotablink+' 
', 'after') taplain = ta.next('.plainvalue') taplain.one('.pseudotablink').on('focus', M.gradingform_guideeditor.clickanywhere) if (tb) { tb.get('parentNode').append(''+pseudotablink+' ') tbplain = tb.get('parentNode').one('.plainvalue') tbplain.one('.pseudotablink').on('focus', M.gradingform_guideeditor.clickanywhere) } } if (tb && !tbplain) { tbplain = tb.get('parentNode').one('.plainvalue') } if (!editmode) { // if we need to hide the input fields, copy their contents to plainvalue(s). If description/definition // is empty, display the default text ('Click to edit ...') and add/remove 'empty' CSS class to element var value = Y.Lang.trim(ta.get('value')); if (value.length) { taplain.removeClass('empty') } else if (ta.get('name').indexOf('[shortname]') > 1){ value = M.util.get_string('clicktoeditname', 'gradingform_guide') taplain.addClass('editname') } else { value = M.util.get_string('clicktoedit', 'gradingform_guide') taplain.addClass('empty') } // Replace newlines with
tags, when displaying in the page. taplain.one('.textvalue').set('innerHTML', Y.Escape.html(value).replace(/(?:\r\n|\r|\n)/g, '
')) if (tb) { tbplain.one('.textvalue').set('innerHTML', Y.Escape.html(tb.get('value'))) } // hide/display textarea, textbox and plaintexts taplain.removeClass('hiddenelement') ta.addClass('hiddenelement') if (tb) { tbplain.removeClass('hiddenelement') tb.addClass('hiddenelement') } } else { // if we need to show the input fields, set the width/height for textarea so it fills the cell try { if (ta.get('name').indexOf('[maxscore]') > 1) { ta.setStyle('width', '25px'); } else { var width = parseFloat(ta.get('parentNode').getComputedStyle('width'))-10, height = parseFloat(ta.get('parentNode').getComputedStyle('height')) ta.setStyle('width', Math.max(width,50)+'px') ta.setStyle('height', Math.max(height,30)+'px') } } catch (err) { // this browser do not support 'computedStyle', leave the default size of the textbox } // hide/display textarea, textbox and plaintexts taplain.addClass('hiddenelement') ta.removeClass('hiddenelement') if (tb) { tbplain.addClass('hiddenelement') tb.removeClass('hiddenelement') } } // focus the proper input field in edit mode if (editmode) { ta.focus() } } // handler for clicking on submit buttons within guideeditor element. Adds/deletes/rearranges criteria/comments on client side M.gradingform_guideeditor.buttonclick = function(e, confirmed) { var Y = M.gradingform_guideeditor.Y var name = M.gradingform_guideeditor.name if (e.target.get('type') != 'submit') { return; } M.gradingform_guideeditor.disablealleditors() var chunks = e.target.get('id').split('-') var section = chunks[1] var action = chunks[chunks.length-1] if (chunks[0] != name || (section != 'criteria' && section != 'comments')) { return; } // prepare the id of the next inserted criterion var elements_str; if (section == 'criteria') { elements_str = '#guide-'+name+' .criteria .criterion' } else if (section == 'comments') { elements_str = '#guide-'+name+' .comments .criterion' } var newid = 0; if (action == 'addcriterion' || action == 'addcomment') { newid = M.gradingform_guideeditor.calculatenewid(elements_str); } if (chunks.length == 3 && (action == 'addcriterion' || action == 'addcomment')) { // ADD NEW CRITERION OR COMMENT var parentel = Y.one('#'+name+'-'+section) if (parentel.one('>tbody')) { parentel = parentel.one('>tbody') } if (section == 'criteria') { var newcriterion = M.gradingform_guideeditor.templates[name]['criterion'] parentel.append(newcriterion.replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, '')) } else if (section == 'comments') { var newcomment = M.gradingform_guideeditor.templates[name]['comment'] parentel.append(newcomment.replace(/\{COMMENT-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, '')) } M.gradingform_guideeditor.addhandlers(); M.gradingform_guideeditor.disablealleditors() M.gradingform_guideeditor.assignclasses(elements_str) // Enable edit mode of the newly added criterion/comment entry. var inputTarget = 'shortname'; if (action == 'addcomment') { inputTarget = 'description'; } var inputTargetId = '#guide-' + name + ' #' + name + '-' + section + '-NEWID' + newid + '-' + inputTarget; M.gradingform_guideeditor.editmode(Y.one(inputTargetId), true); } else if (chunks.length == 4 && action == 'moveup') { // MOVE UP el = Y.one('#'+name+'-'+section+'-'+chunks[2]) if (el.previous()) { el.get('parentNode').insertBefore(el, el.previous()) } M.gradingform_guideeditor.assignclasses(elements_str) } else if (chunks.length == 4 && action == 'movedown') { // MOVE DOWN el = Y.one('#'+name+'-'+section+'-'+chunks[2]) if (el.next()) { el.get('parentNode').insertBefore(el.next(), el) } M.gradingform_guideeditor.assignclasses(elements_str) } else if (chunks.length == 4 && action == 'delete') { // DELETE if (confirmed) { Y.one('#'+name+'-'+section+'-'+chunks[2]).remove() M.gradingform_guideeditor.assignclasses(elements_str) } else { require(['core/notification', 'core/str'], function(Notification, Str) { Notification.saveCancelPromise( Str.get_string('confirmation', 'admin'), Str.get_string('confirmdeletecriterion', 'gradingform_guide'), Str.get_string('yes', 'moodle') ).then(function() { M.gradingform_guideeditor.buttonclick.apply(this, [e, true]); return; }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); } } else { // unknown action return; } e.preventDefault(); } // properly set classes (first/last/odd/even) and/or criterion sortorder for elements Y.all(elements_str) M.gradingform_guideeditor.assignclasses = function (elements_str) { var elements = M.gradingform_guideeditor.Y.all(elements_str) for (var i=0; i. /** * Advanced grading methods support * * @package core_grading * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); use core_grades\component_gradeitems; /** * Factory method returning an instance of the grading manager * * There are basically ways how to use this factory method. If the area record * id is known to the caller, get the manager for that area by providing just * the id. If the area record id is not know, the context, component and area name * can be provided. Note that null values are allowed in the second case as the context, * component and the area name can be set explicitly later. * * @category grading * @example $manager = get_grading_manager($areaid); * @example $manager = get_grading_manager(context_system::instance()); * @example $manager = get_grading_manager($context, 'mod_assign', 'submission'); * @param stdClass|int|null $context_or_areaid if $areaid is passed, no other parameter is needed * @param string|null $component the frankenstyle name of the component * @param string|null $area the name of the gradable area * @return grading_manager */ function get_grading_manager($context_or_areaid = null, $component = null, $area = null) { global $DB; $manager = new grading_manager(); if (is_object($context_or_areaid)) { $context = $context_or_areaid; } else { $context = null; if (is_numeric($context_or_areaid)) { $manager->load($context_or_areaid); return $manager; } } if (!is_null($context)) { $manager->set_context($context); } if (!is_null($component)) { $manager->set_component($component); } if (!is_null($area)) { $manager->set_area($area); } return $manager; } /** * General class providing access to common grading features * * Grading manager provides access to the particular grading method controller * in that area. * * Fully initialized instance of the grading manager operates over a single * gradable area. It is possible to work with a partially initialized manager * that knows just context and component without known area, for example. * It is also possible to change context, component and area of an existing * manager. Such pattern is used when copying form definitions, for example. * * @package core_grading * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @category grading */ class grading_manager { /** @var stdClass the context */ protected $context; /** @var string the frankenstyle name of the component */ protected $component; /** @var string the name of the gradable area */ protected $area; /** @var stdClass|false|null the raw record from {grading_areas}, false if does not exist, null if invalidated cache */ private $areacache = null; /** * Returns grading manager context * * @return stdClass grading manager context */ public function get_context() { return $this->context; } /** * Sets the context the manager operates on * * @param stdClass $context */ public function set_context(stdClass $context) { $this->areacache = null; $this->context = $context; } /** * Returns grading manager component * * @return string grading manager component */ public function get_component() { return $this->component; } /** * Sets the component the manager operates on * * @param string $component the frankenstyle name of the component */ public function set_component($component) { $this->areacache = null; list($type, $name) = core_component::normalize_component($component); $this->component = $type.'_'.$name; } /** * Returns grading manager area name * * @return string grading manager area name */ public function get_area() { return $this->area; } /** * Sets the area the manager operates on * * @param string $area the name of the gradable area */ public function set_area($area) { $this->areacache = null; $this->area = $area; } /** * Returns a text describing the context and the component * * At the moment this works for gradable areas in course modules. In the future, this * method should be improved so it works for other contexts (blocks, gradebook items etc) * or subplugins. * * @return string */ public function get_component_title() { $this->ensure_isset(array('context', 'component')); if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) { if ($this->get_component() == 'core_grading') { $title = ''; // we are in the bank UI } else { throw new coding_exception('Unsupported component at the system context'); } } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) { list($context, $course, $cm) = get_context_info_array($this->get_context()->id); if ($cm && strval($cm->name) !== '') { $title = format_string($cm->name, true, array('context' => $context)); } else { debugging('Gradable areas are currently supported at the course module level only', DEBUG_DEVELOPER); $title = $this->get_component(); } } else { throw new coding_exception('Unsupported gradable area context level'); } return $title; } /** * Returns the localized title of the currently set area * * @return string */ public function get_area_title() { if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) { return ''; } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) { $this->ensure_isset(array('context', 'component', 'area')); $areas = $this->get_available_areas(); if (array_key_exists($this->get_area(), $areas)) { return $areas[$this->get_area()]; } else { debugging('Unknown area!'); return '???'; } } else { throw new coding_exception('Unsupported context level'); } } /** * Loads the gradable area info from the database * * @param int $areaid */ public function load($areaid) { global $DB; $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST); $this->context = context::instance_by_id($this->areacache->contextid, MUST_EXIST); $this->component = $this->areacache->component; $this->area = $this->areacache->areaname; } /** * Returns the list of installed grading plugins together, optionally extended * with a simple direct grading. * * @param bool $includenone should the 'Simple direct grading' be included * @return array of the (string)name => (string)localized title of the method */ public static function available_methods($includenone = true) { if ($includenone) { $list = array('' => get_string('gradingmethodnone', 'core_grading')); } else { $list = array(); } foreach (core_component::get_plugin_list('gradingform') as $name => $location) { $list[$name] = get_string('pluginname', 'gradingform_'.$name); } return $list; } /** * Returns the list of available grading methods in the given context * * Currently this is just a static list obtained from {@link self::available_methods()}. * In the future, the list of available methods may be controlled per-context. * * Requires the context property to be set in advance. * * @param bool $includenone should the 'Simple direct grading' be included * @return array of the (string)name => (string)localized title of the method */ public function get_available_methods($includenone = true) { $this->ensure_isset(array('context')); return self::available_methods($includenone); } /** * Returns the list of gradable areas provided by the given component * * This performs a callback to the library of the relevant plugin to obtain * the list of supported areas. * * @param string $component normalized component name * @return array of (string)areacode => (string)localized title of the area */ public static function available_areas($component) { global $CFG; if (component_gradeitems::defines_advancedgrading_itemnames_for_component($component)) { $result = []; foreach (component_gradeitems::get_advancedgrading_itemnames_for_component($component) as $itemnumber => $itemname) { $result[$itemname] = get_string("gradeitem:{$itemname}", $component); } return $result; } list($plugintype, $pluginname) = core_component::normalize_component($component); if ($component === 'core_grading') { return array(); } else if ($plugintype === 'mod') { $callbackfunction = "grading_areas_list"; if (component_callback_exists($component, $callbackfunction)) { debugging( "Components supporting advanced grading should be updated to implement the component_gradeitems class", DEBUG_DEVELOPER ); return component_callback($component, $callbackfunction, [], []); } } else { throw new coding_exception('Unsupported area location'); } } /** * Returns the list of gradable areas in the given context and component * * This performs a callback to the library of the relevant plugin to obtain * the list of supported areas. * @return array of (string)areacode => (string)localized title of the area */ public function get_available_areas() { global $CFG; $this->ensure_isset(array('context', 'component')); if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) { if ($this->get_component() !== 'core_grading') { throw new coding_exception('Unsupported component at the system context'); } else { return array(); } } else if ($this->get_context()->contextlevel == CONTEXT_MODULE) { $modulecontext = $this->get_context(); $coursecontext = $modulecontext->get_course_context(); $cm = get_fast_modinfo($coursecontext->instanceid)->get_cm($modulecontext->instanceid); return self::available_areas("mod_{$cm->modname}"); } else { throw new coding_exception('Unsupported gradable area context level'); } } /** * Returns the currently active grading method in the gradable area * * @return string|null the name of the grading plugin of null if it has not been set */ public function get_active_method() { global $DB; $this->ensure_isset(array('context', 'component', 'area')); // get the current grading area record if it exists if (is_null($this->areacache)) { $this->areacache = $DB->get_record('grading_areas', array( 'contextid' => $this->context->id, 'component' => $this->component, 'areaname' => $this->area), '*', IGNORE_MISSING); } if ($this->areacache === false) { // no area record yet return null; } return $this->areacache->activemethod; } /** * Sets the currently active grading method in the gradable area * * @param string $method the method name, eg 'rubric' (must be available) * @return bool true if the method changed or was just set, false otherwise */ public function set_active_method($method) { global $DB; $this->ensure_isset(array('context', 'component', 'area')); // make sure the passed method is empty or a valid plugin name if (empty($method)) { $method = null; } else { if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) { throw new moodle_exception('invalid_method_name', 'core_grading'); } $available = $this->get_available_methods(false); if (!array_key_exists($method, $available)) { throw new moodle_exception('invalid_method_name', 'core_grading'); } } // get the current grading area record if it exists if (is_null($this->areacache)) { $this->areacache = $DB->get_record('grading_areas', array( 'contextid' => $this->context->id, 'component' => $this->component, 'areaname' => $this->area), '*', IGNORE_MISSING); } $methodchanged = false; if ($this->areacache === false) { // no area record yet, create one with the active method set $area = array( 'contextid' => $this->context->id, 'component' => $this->component, 'areaname' => $this->area, 'activemethod' => $method); $DB->insert_record('grading_areas', $area); $methodchanged = true; } else { // update the existing record if needed if ($this->areacache->activemethod !== $method) { $DB->set_field('grading_areas', 'activemethod', $method, array('id' => $this->areacache->id)); $methodchanged = true; } } $this->areacache = null; return $methodchanged; } /** * Extends the settings navigation with the grading settings * * This function is called when the context for the page is an activity module with the * FEATURE_ADVANCED_GRADING and the user has the permission moodle/grade:managegradingforms. * * @param settings_navigation $settingsnav {@link settings_navigation} * @param navigation_node $modulenode {@link navigation_node} */ public function extend_settings_navigation(settings_navigation $settingsnav, ?navigation_node $modulenode=null) { $this->ensure_isset(array('context', 'component')); $areas = $this->get_available_areas(); if (empty($areas)) { // no money, no funny return; } else { // make just a single node for the management screen $areatitle = reset($areas); $areaname = key($areas); $this->set_area($areaname); $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'), $this->get_management_url(), settings_navigation::TYPE_CUSTOM, null, 'advgrading'); } } /** * Extends the module navigation with the advanced grading information * * This function is called when the context for the page is an activity module with the * FEATURE_ADVANCED_GRADING. * * @param global_navigation $navigation * @param navigation_node $modulenode */ public function extend_navigation(global_navigation $navigation, ?navigation_node $modulenode=null) { $this->ensure_isset(array('context', 'component')); $areas = $this->get_available_areas(); foreach ($areas as $areaname => $areatitle) { $this->set_area($areaname); if ($controller = $this->get_active_controller()) { $controller->extend_navigation($navigation, $modulenode); } } } /** * Returns the given method's controller in the gradable area * * @param string $method the method name, eg 'rubric' (must be available) * @return gradingform_controller */ public function get_controller($method) { global $CFG, $DB; $this->ensure_isset(array('context', 'component', 'area')); // make sure the passed method is a valid plugin name if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) { throw new moodle_exception('invalid_method_name', 'core_grading'); } $available = $this->get_available_methods(false); if (!array_key_exists($method, $available)) { throw new moodle_exception('invalid_method_name', 'core_grading'); } // get the current grading area record if it exists if (is_null($this->areacache)) { $this->areacache = $DB->get_record('grading_areas', array( 'contextid' => $this->context->id, 'component' => $this->component, 'areaname' => $this->area), '*', IGNORE_MISSING); } if ($this->areacache === false) { // no area record yet, create one $area = array( 'contextid' => $this->context->id, 'component' => $this->component, 'areaname' => $this->area); $areaid = $DB->insert_record('grading_areas', $area); // reload the cache $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST); } require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php'); $classname = 'gradingform_'.$method.'_controller'; return new $classname($this->context, $this->component, $this->area, $this->areacache->id); } /** * Returns the controller for the active method if it is available * * @return null|gradingform_controller */ public function get_active_controller() { if ($gradingmethod = $this->get_active_method()) { $controller = $this->get_controller($gradingmethod); if ($controller->is_form_available()) { return $controller; } } return null; } /** * Returns the URL of the grading area management page * * @param moodle_url $returnurl optional URL of the page where the user should be sent back to * @return moodle_url */ public function get_management_url(?moodle_url $returnurl = null) { $this->ensure_isset(array('context', 'component')); if ($this->areacache) { $params = array('areaid' => $this->areacache->id); } else { $params = array('contextid' => $this->context->id, 'component' => $this->component); if ($this->area) { $params['area'] = $this->area; } } if (!is_null($returnurl)) { $params['returnurl'] = $returnurl->out(false); } return new moodle_url('/grade/grading/manage.php', $params); } /** * Creates a new shared area to hold a grading form template * * Shared area are implemented as virtual gradable areas at the system level context * with the component set to core_grading and unique random area name. * * @param string $method the name of the plugin we create the area for * @return int the new area id */ public function create_shared_area($method) { global $DB; // generate some unique random name for the new area $name = $method . '_' . sha1(rand().uniqid($method, true)); // create new area record $area = array( 'contextid' => context_system::instance()->id, 'component' => 'core_grading', 'areaname' => $name, 'activemethod' => $method); return $DB->insert_record('grading_areas', $area); } /** * Removes all data associated with the given context * * This is called by {@link context::delete_content()} * * @param int $contextid context id */ public static function delete_all_for_context($contextid) { global $DB; $areaids = $DB->get_fieldset_select('grading_areas', 'id', 'contextid = ?', array($contextid)); $methods = array_keys(self::available_methods(false)); foreach($areaids as $areaid) { $manager = get_grading_manager($areaid); foreach ($methods as $method) { $controller = $manager->get_controller($method); $controller->delete_definition(); } } $DB->delete_records_list('grading_areas', 'id', $areaids); } /** * Helper method to tokenize the given string * * Splits the given string into smaller strings. This is a helper method for * full text searching in grading forms. If the given string is surrounded with * double quotes, the resulting array consists of a single item containing the * quoted content. * * Otherwise, string like 'grammar, english language' would be tokenized into * the three tokens 'grammar', 'english', 'language'. * * One-letter tokens like are dropped in non-phrase mode. Repeated tokens are * returned just once. * * @param string $needle * @return array */ public static function tokenize($needle) { // check if we are searching for the exact phrase if (preg_match('/^[\s]*"[\s]*(.*?)[\s]*"[\s]*$/', $needle, $matches)) { $token = $matches[1]; if ($token === '') { return array(); } else { return array($token); } } // split the needle into smaller parts separated by non-word characters $tokens = preg_split("/\W/u", $needle); // keep just non-empty parts $tokens = array_filter($tokens); // distinct $tokens = array_unique($tokens); // drop one-letter tokens foreach ($tokens as $ix => $token) { if (strlen($token) == 1) { unset($tokens[$ix]); } } return array_values($tokens); } // ////////////////////////////////////////////////////////////////////////// /** * Make sure that the given properties were set to some not-null value * * @param array $properties the list of properties * @throws coding_exception */ private function ensure_isset(array $properties) { foreach ($properties as $property) { if (!isset($this->$property)) { throw new coding_exception('The property "'.$property.'" is not set.'); } } } } PK'\T7** manage.phpnu[. /** * A single gradable area management page * * This page alows the user to set the current active method in the given * area, provides access to the plugin editor and allows user to save the * current form as a template or re-use some existing form. * * @package core_grading * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../config.php'); require_once($CFG->dirroot.'/grade/grading/lib.php'); // identify gradable area by its id $areaid = optional_param('areaid', null, PARAM_INT); // alternatively the context, component and areaname must be provided $contextid = optional_param('contextid', null, PARAM_INT); $component = optional_param('component', null, PARAM_COMPONENT); $area = optional_param('area', null, PARAM_AREA); // keep the caller's URL so that we know where to send the user finally $returnurl = optional_param('returnurl', null, PARAM_LOCALURL); // active method selector $setmethod = optional_param('setmethod', null, PARAM_PLUGIN); // publish the given form definition as a new template in the forms bank $shareform = optional_param('shareform', null, PARAM_INT); // delete the given form definition $deleteform = optional_param('deleteform', null, PARAM_INT); // consider the required action as confirmed $confirmed = optional_param('confirmed', false, PARAM_BOOL); // a message to display, typically a previous action's result $message = optional_param('message', null, PARAM_NOTAGS); if (!is_null($areaid)) { // get manager by id $manager = get_grading_manager($areaid); } else { // get manager by context and component if (is_null($contextid) or is_null($component) or is_null($area)) { throw new coding_exception('The caller script must identify the gradable area.'); } $context = context::instance_by_id($contextid, MUST_EXIST); $manager = get_grading_manager($context, $component, $area); } if ($manager->get_context()->contextlevel < CONTEXT_COURSE) { throw new coding_exception('Unsupported gradable area context level'); } // get the currently active method $method = $manager->get_active_method(); list($context, $course, $cm) = get_context_info_array($manager->get_context()->id); require_login($course, true, $cm); require_capability('moodle/grade:managegradingforms', $context); if (!empty($returnurl)) { $returnurl = new moodle_url($returnurl); } else { $returnurl = null; } $PAGE->set_url($manager->get_management_url($returnurl)); navigation_node::override_active_url($manager->get_management_url()); $PAGE->set_title(get_string('gradingmanagement', 'core_grading')); $PAGE->set_heading(get_string('gradingmanagement', 'core_grading')); // We don't need to show the default header on a management page. $PAGE->activityheader->disable(); $output = $PAGE->get_renderer('core_grading'); // process the eventual change of the active grading method if (!empty($setmethod)) { require_sesskey(); if ($setmethod == 'none') { // here we expect that noone would actually want to call their plugin as 'none' $setmethod = null; } $manager->set_active_method($setmethod); redirect($PAGE->url); } // publish the form as a template if (!empty($shareform)) { require_capability('moodle/grade:sharegradingforms', context_system::instance()); $controller = $manager->get_controller($method); $definition = $controller->get_definition(); if (!$confirmed) { // let the user confirm they understand what they are doing (haha ;-) echo $output->header(); echo $output->confirm(get_string('manageactionshareconfirm', 'core_grading', s($definition->name)), new moodle_url($PAGE->url, array('shareform' => $shareform, 'confirmed' => 1)), $PAGE->url); echo $output->footer(); die(); } else { require_sesskey(); $newareaid = $manager->create_shared_area($method); $targetarea = get_grading_manager($newareaid); $targetcontroller = $targetarea->get_controller($method); $targetcontroller->update_definition($controller->get_definition_copy($targetcontroller)); $DB->set_field('grading_definitions', 'timecopied', time(), array('id' => $definition->id)); redirect(new moodle_url($PAGE->url, array('message' => get_string('manageactionsharedone', 'core_grading')))); } } // delete the form definition if (!empty($deleteform)) { $controller = $manager->get_controller($method); $definition = $controller->get_definition(); if (!$confirmed) { // let the user confirm they understand the consequences (also known as WTF-effect) echo $output->header(); echo $output->confirm(markdown_to_html(get_string('manageactiondeleteconfirm', 'core_grading', array( 'formname' => s($definition->name), 'component' => $manager->get_component_title(), 'area' => $manager->get_area_title()))), new moodle_url($PAGE->url, array('deleteform' => $deleteform, 'confirmed' => 1)), $PAGE->url); echo $output->footer(); die(); } else { require_sesskey(); $controller->delete_definition(); redirect(new moodle_url($PAGE->url, array('message' => get_string('manageactiondeletedone', 'core_grading')))); } } echo $output->header(); if (!empty($message)) { echo $output->management_message($message); } if ($PAGE->has_secondary_navigation()) { echo $output->heading(get_string('gradingmanagement', 'core_grading')); } else { echo $output->heading(get_string('gradingmanagementtitle', 'core_grading', array( 'component' => $manager->get_component_title(), 'area' => $manager->get_area_title()))); } // display the active grading method information and selector echo $output->management_method_selector($manager, $PAGE->url); // get the currently active method's controller if (!empty($method)) { $controller = $manager->get_controller($method); // display relevant actions echo $output->container_start('actions'); if ($controller->is_form_defined()) { $definition = $controller->get_definition(); // icon to edit the form definition echo $output->management_action_icon($controller->get_editor_url($returnurl), get_string('manageactionedit', 'core_grading'), 'b/document-edit'); // icon to delete the current form definition echo $output->management_action_icon(new moodle_url($PAGE->url, array('deleteform' => $definition->id)), get_string('manageactiondelete', 'core_grading'), 'b/edit-delete'); // icon to save the form as a new template if (has_capability('moodle/grade:sharegradingforms', context_system::instance())) { if (empty($definition->copiedfromid)) { $hasoriginal = false; } else { $hasoriginal = $DB->record_exists('grading_definitions', array('id' => $definition->copiedfromid)); } if (!$controller->is_form_available()) { // drafts can not be shared $allowshare = false; } else if (!$hasoriginal) { // was created from scratch or is orphaned if (empty($definition->timecopied)) { // was never shared before $allowshare = true; } else if ($definition->timemodified > $definition->timecopied) { // was modified since last time shared $allowshare = true; } else { // was not modified since last time shared $allowshare = false; } } else { // was created from a template and the template still exists if ($definition->timecreated == $definition->timemodified) { // was not modified since created $allowshare = false; } else if (empty($definition->timecopied)) { // was modified but was not re-shared yet $allowshare = true; } else if ($definition->timemodified > $definition->timecopied) { // was modified since last time re-shared $allowshare = true; } else { // was not modified since last time re-shared $allowshare = false; } } if ($allowshare) { echo $output->management_action_icon(new moodle_url($PAGE->url, array('shareform' => $definition->id)), get_string('manageactionshare', 'core_grading'), 'b/bookmark-new'); } } } else { echo $output->management_action_icon($controller->get_editor_url($returnurl), get_string('manageactionnew', 'core_grading'), 'b/document-new'); $pickurl = new moodle_url('/grade/grading/pick.php', array('targetid' => $controller->get_areaid())); if (!is_null($returnurl)) { $pickurl->param('returnurl', $returnurl->out(false)); } echo $output->management_action_icon($pickurl, get_string('manageactionclone', 'core_grading'), 'b/edit-copy'); } echo $output->container_end(); // display the message if the form is currently not available (if applicable) if ($message = $controller->form_unavailable_notification()) { echo $output->notification($message); } // display the grading form preview if ($controller->is_form_defined()) { if ($definition->status == gradingform_controller::DEFINITION_STATUS_READY) { $tag = html_writer::tag('span', get_string('statusready', 'core_grading'), array('class' => 'status ready')); } else { $tag = html_writer::tag('span', get_string('statusdraft', 'core_grading'), array('class' => 'status draft')); } echo $output->heading(format_string($definition->name) . ' ' . $tag, 3, 'definition-name'); echo $output->box($controller->render_preview($PAGE), 'definition-preview'); } } echo $output->footer(); PK'\+classes/privacy/gradingform_provider_v2.phpnu[. /** * Privacy class for requesting user data. * * @package core_grading * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_grading\privacy; defined('MOODLE_INTERNAL') || die(); interface gradingform_provider_v2 extends \core_privacy\local\request\plugin\subsystem_provider, \core_privacy\local\request\shared_userlist_provider { /** * Export user data relating to an instance ID. * * @param \context $context Context to use with the export writer. * @param int $instanceid The instance ID to export data for. * @param array $subcontext The directory to export this data to. */ public static function export_gradingform_instance_data(\context $context, int $instanceid, array $subcontext); /** * Deletes all user data related to the provided instance IDs. * * @param array $instanceids The instance IDs to delete information from. */ public static function delete_gradingform_for_instances(array $instanceids); } PK'\!a@@classes/privacy/provider.phpnu[. /** * Privacy class for requesting user data. * * @package core_grading * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_grading\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\transform; use \core_privacy\local\request\writer; use \core_privacy\manager; /** * Privacy class for requesting user data. * * @copyright 2018 Sara Arjona * @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\core_userlist_provider, \core_privacy\local\request\subsystem\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_database_table('grading_definitions', [ 'method' => 'privacy:metadata:grading_definitions:method', 'areaid' => 'privacy:metadata:grading_definitions:areaid', 'name' => 'privacy:metadata:grading_definitions:name', 'description' => 'privacy:metadata:grading_definitions:description', 'status' => 'privacy:metadata:grading_definitions:status', 'copiedfromid' => 'privacy:metadata:grading_definitions:copiedfromid', 'timecopied' => 'privacy:metadata:grading_definitions:timecopied', 'timecreated' => 'privacy:metadata:grading_definitions:timecreated', 'usercreated' => 'privacy:metadata:grading_definitions:usercreated', 'timemodified' => 'privacy:metadata:grading_definitions:timemodified', 'usermodified' => 'privacy:metadata:grading_definitions:usermodified', 'options' => 'privacy:metadata:grading_definitions:options', ], 'privacy:metadata:grading_definitions'); $collection->add_database_table('grading_instances', [ 'raterid' => 'privacy:metadata:grading_instances:raterid', 'rawgrade' => 'privacy:metadata:grading_instances:rawgrade', 'status' => 'privacy:metadata:grading_instances:status', 'feedback' => 'privacy:metadata:grading_instances:feedback', 'feedbackformat' => 'privacy:metadata:grading_instances:feedbackformat', 'timemodified' => 'privacy:metadata:grading_instances:timemodified', ], 'privacy:metadata:grading_instances'); // Link to subplugin. $collection->add_plugintype_link('gradingform', [], 'privacy:metadata:gradingformpluginsummary'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { $contextlist = new contextlist(); $sql = "SELECT c.id FROM {context} c JOIN {grading_areas} a ON a.contextid = c.id JOIN {grading_definitions} d ON d.areaid = a.id LEFT JOIN {grading_instances} i ON i.definitionid = d.id AND i.raterid = :raterid WHERE c.contextlevel = :contextlevel AND (d.usercreated = :usercreated OR d.usermodified = :usermodified OR i.id IS NOT NULL)"; $params = [ 'usercreated' => $userid, 'usermodified' => $userid, 'raterid' => $userid, 'contextlevel' => CONTEXT_MODULE ]; $contextlist->add_from_sql($sql, $params); 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(\core_privacy\local\request\userlist $userlist) { $context = $userlist->get_context(); if ($context->contextlevel != CONTEXT_MODULE) { return; } $params = ['contextid' => $context->id]; $sql = "SELECT d.usercreated, d.usermodified FROM {grading_definitions} d JOIN {grading_areas} a ON a.id = d.areaid WHERE a.contextid = :contextid"; $userlist->add_from_sql('usercreated', $sql, $params); $userlist->add_from_sql('usermodified', $sql, $params); $sql = "SELECT i.raterid FROM {grading_definitions} d JOIN {grading_areas} a ON a.id = d.areaid JOIN {grading_instances} i ON i.definitionid = d.id WHERE a.contextid = :contextid"; $userlist->add_from_sql('raterid', $sql, $params); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { // Remove contexts different from MODULE. $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) { if ($context->contextlevel == CONTEXT_MODULE) { $carry[] = $context; } return $carry; }, []); if (empty($contexts)) { return; } $userid = $contextlist->get_user()->id; $subcontext = [get_string('gradingmethod', 'grading')]; foreach ($contexts as $context) { // Export grading definitions created or modified on this context. self::export_definitions($context, $subcontext, $userid); } } /** * Export all user data related to a context and itemid. * * @param \context $context Context to export on. * @param int $itemid Item ID to export on. * @param array $subcontext Directory location to export to. */ public static function export_item_data(\context $context, int $itemid, array $subcontext) { global $DB; $sql = "SELECT gi.id AS instanceid, gd.id AS definitionid, gd.method FROM {grading_areas} ga JOIN {grading_definitions} gd ON gd.areaid = ga.id JOIN {grading_instances} gi ON gi.definitionid = gd.id AND gi.itemid = :itemid WHERE ga.contextid = :contextid"; $params = [ 'itemid' => $itemid, 'contextid' => $context->id, ]; $records = $DB->get_recordset_sql($sql, $params); foreach ($records as $record) { $instancedata = manager::component_class_callback( "gradingform_{$record->method}", gradingform_provider_v2::class, 'export_gradingform_instance_data', [$context, $record->instanceid, $subcontext] ); } $records->close(); } /** * Deletes all user data related to a context and possibly an itemid. * * @param \context $context The context to delete on. * @param int|null $itemid An optional item ID to refine the deletion. */ public static function delete_instance_data(\context $context, ?int $itemid = null) { if (is_null($itemid)) { self::delete_data_for_instances($context); } else { self::delete_data_for_instances($context, [$itemid]); } } /** * Deletes all user data related to a context and possibly itemids. * * @param \context $context The context to delete on. * @param array $itemids An optional list of item IDs to refine the deletion. */ public static function delete_data_for_instances(\context $context, array $itemids = []) { global $DB; $itemsql = ''; $params = ['contextid' => $context->id]; if (!empty($itemids)) { list($itemsql, $itemparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED); $params = array_merge($params, $itemparams); $itemsql = "AND itemid $itemsql"; } $sql = "SELECT gi.id AS instanceid, gd.id, gd.method FROM {grading_definitions} gd JOIN {grading_instances} gi ON gi.definitionid = gd.id JOIN {grading_areas} ga ON ga.id = gd.areaid WHERE ga.contextid = :contextid $itemsql"; $records = $DB->get_records_sql($sql, $params); if ($records) { $firstrecord = current($records); $method = $firstrecord->method; $instanceids = array_map(function($record) { return $record->instanceid; }, $records); manager::component_class_callback( "gradingform_{$method}", gradingform_provider_v2::class, 'delete_gradingform_for_instances', [$instanceids]); // Delete grading_instances rows. $DB->delete_records_list('grading_instances', 'id', $instanceids); } } /** * Exports the data related to grading definitions within the specified context/subcontext. * * @param \context $context Context owner of the data. * @param array $subcontext Subcontext owner of the data. * @param int $userid The user whose information is to be exported. */ protected static function export_definitions(\context $context, array $subcontext, int $userid = 0) { global $DB; $join = "JOIN {grading_areas} a ON a.id = d.areaid JOIN {context} c ON a.contextid = c.id AND c.contextlevel = :contextlevel"; $select = 'a.contextid = :contextid'; $params = [ 'contextlevel' => CONTEXT_MODULE, 'contextid' => $context->id ]; if (!empty($userid)) { $join .= ' LEFT JOIN {grading_instances} i ON i.definitionid = d.id AND i.raterid = :raterid'; $select .= ' AND (usercreated = :usercreated OR usermodified = :usermodified OR i.id IS NOT NULL)'; $params['usercreated'] = $userid; $params['usermodified'] = $userid; $params['raterid'] = $userid; } $sql = "SELECT gd.id, gd.method, gd.name, gd.description, gd.timecopied, gd.timecreated, gd.usercreated, gd.timemodified, gd.usermodified FROM ( SELECT DISTINCT d.id FROM {grading_definitions} d $join WHERE $select ) ids JOIN {grading_definitions} gd ON gd.id = ids.id"; $definitions = $DB->get_recordset_sql($sql, $params); $defdata = []; foreach ($definitions as $definition) { $tmpdata = [ 'method' => $definition->method, 'name' => $definition->name, 'description' => $definition->description, 'timecreated' => transform::datetime($definition->timecreated), 'usercreated' => transform::user($definition->usercreated), 'timemodified' => transform::datetime($definition->timemodified), 'usermodified' => transform::user($definition->usermodified), ]; if (!empty($definition->timecopied)) { $tmpdata['timecopied'] = transform::datetime($definition->timecopied); } $defdata[] = (object) $tmpdata; // Export grading_instances information. self::export_grading_instances($context, $subcontext, $definition->id, $userid); } $definitions->close(); if (!empty($defdata)) { $data = (object) [ 'definitions' => $defdata, ]; writer::with_context($context)->export_data($subcontext, $data); } } /** * Exports the data related to grading instances within the specified definition. * * @param \context $context Context owner of the data. * @param array $subcontext Subcontext owner of the data. * @param int $definitionid The definition ID whose grading instance information is to be exported. * @param int $userid The user whose information is to be exported. */ protected static function export_grading_instances(\context $context, array $subcontext, int $definitionid, int $userid = 0) { global $DB; $params = ['definitionid' => $definitionid]; if (!empty($userid)) { $params['raterid'] = $userid; } $instances = $DB->get_recordset('grading_instances', $params); $instancedata = []; foreach ($instances as $instance) { // TODO: Get the status name (instead of the ID). $tmpdata = [ 'rawgrade' => $instance->rawgrade, 'status' => $instance->status, 'feedback' => $instance->feedback, 'feedbackformat' => $instance->feedbackformat, 'timemodified' => transform::datetime($instance->timemodified), ]; $instancedata[] = (object) $tmpdata; } $instances->close(); if (!empty($instancedata)) { $data = (object) [ 'instances' => $instancedata, ]; writer::with_context($context)->export_related_data($subcontext, 'gradinginstances', $data); } } /** * No deletion of the advanced grading is done. * * @param \context $context the context to delete in. */ public static function delete_data_for_all_users_in_context(\context $context) { // The only information left to be deleted here is the grading definitions. Currently we are not deleting these. } /** * Deletion of data in this provider is only related to grades and so can not be * deleted for the creator of the advanced grade criteria. * * @param approved_contextlist $contextlist a list of contexts approved for deletion. */ public static function delete_data_for_user(approved_contextlist $contextlist) { // The only information left to be deleted here is the grading definitions. Currently we are not deleting these. } /** * 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(\core_privacy\local\request\approved_userlist $userlist) { // The only information left to be deleted here is the grading definitions. Currently we are not deleting these. } } PK'\sWE/classes/privacy/gradingform_legacy_polyfill.phpnu[. /** * This file contains the polyfill to allow a plugin to operate with Moodle 3.3 up. * * @package core_grading * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_grading\privacy; defined('MOODLE_INTERNAL') || die(); /** * The trait used to provide backwards compatability for third-party plugins. * * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait gradingform_legacy_polyfill { /** * Export user data relating to an instance ID. * * @param \context $context Context to use with the export writer. * @param int $instanceid The instance ID to export data for. * @param array $subcontext The directory to export this data to. */ public static function export_gradingform_instance_data(\context $context, int $instanceid, array $subcontext) { static::_export_gradingform_instance_data($context, $instanceid, $subcontext); } /** * Deletes all user data related to the provided instance IDs. * * @param array $instanceids The instance IDs to delete information from. */ public static function delete_gradingform_for_instances(array $instanceids) { static::_delete_gradingform_for_instances($instanceids); } } PK'\Dyui/manage/manage.jsnu[/** * YUI module for advanced grading methods - the manage page * * @author David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ YUI.add('moodle-core_grading-manage', function(Y) { var MANAGE = function() { MANAGE.superclass.constructor.apply(this, arguments); } Y.extend(MANAGE, Y.Base, { initializer : function(config) { this.setup_messagebox(); }, setup_messagebox : function() { Y.one('#actionresultmessagebox span').setContent(M.util.get_string('clicktoclose', 'core_grading')); Y.one('#actionresultmessagebox').on('click', function(e) { e.halt(); var box = e.currentTarget; var anim = new Y.Anim({ node: box, duration: 1, to: { opacity: 0, height: 0 }, }); anim.run(); anim.on('end', function() { var box = this.get('node'); // this === anim box.remove(true); }); }); } }, { NAME : 'grading_manage_page', ATTRS : { } }); M.core_grading = M.core_grading || {}; M.core_grading.init_manage = function(config) { return new MANAGE(config); } }, '@VERSION@', { requires:['base', 'anim'] }); PK'\[,,pick.phpnu[. /** * Allows to choose a form from the list of available templates * * @package core_grading * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../config.php'); require_once($CFG->dirroot.'/grade/grading/lib.php'); require_once($CFG->dirroot.'/grade/grading/pick_form.php'); $targetid = required_param('targetid', PARAM_INT); // area we are coming from $pick = optional_param('pick', null, PARAM_INT); // create new form from this template $remove = optional_param('remove', null, PARAM_INT); // remove this template $confirmed = optional_param('confirmed', false, PARAM_BOOL); // is the action confirmed // the manager of the target area $targetmanager = get_grading_manager($targetid); if ($targetmanager->get_context()->contextlevel < CONTEXT_COURSE) { throw new coding_exception('Unsupported gradable area context level'); } // currently active method in the target area $method = $targetmanager->get_active_method(); $targetcontroller = $targetmanager->get_controller($method); $targetcontrollerclass = get_class($targetcontroller); // make sure there is no such form defined in the target area if ($targetcontroller->is_form_defined()) { redirect(new moodle_url('/grade/grading/manage.php', array('areaid' => $targetid))); } list($context, $course, $cm) = get_context_info_array($targetmanager->get_context()->id); require_login($course, true, $cm); require_capability('moodle/grade:managegradingforms', $context); // user's capability in the templates bank $canshare = has_capability('moodle/grade:sharegradingforms', context_system::instance()); $canmanage = has_capability('moodle/grade:managesharedforms', context_system::instance()); // setup the page $PAGE->set_url(new moodle_url('/grade/grading/pick.php', array('targetid' => $targetid))); navigation_node::override_active_url($targetmanager->get_management_url()); $PAGE->set_title(get_string('gradingmanagement', 'core_grading')); $PAGE->set_heading(get_string('gradingmanagement', 'core_grading')); $output = $PAGE->get_renderer('core_grading'); // process picking a template if ($pick) { $sourceid = $DB->get_field('grading_definitions', 'areaid', array('id' => $pick), MUST_EXIST); $sourcemanager = get_grading_manager($sourceid); $sourcecontroller = $sourcemanager->get_controller($method); if (!$sourcecontroller->is_shared_template() and !$sourcecontroller->is_own_form()) { // note that we don't actually check whether the user has still the capability // moodle/grade:managegradingforms in the source area. so when users loose // their teacher role in a course, they can't access the course but they can // still copy the forms they have created there. throw new moodle_exception('attempt_to_pick_others_form', 'core_grading'); } if (!$sourcecontroller->is_form_defined()) { throw new moodle_exception('form_definition_mismatch', 'core_grading'); } $definition = $sourcecontroller->get_definition(); if (!$confirmed) { echo $output->header(); echo $output->confirm(get_string('templatepickconfirm', 'core_grading',array( 'formname' => s($definition->name), 'component' => $targetmanager->get_component_title(), 'area' => $targetmanager->get_area_title())), new moodle_url($PAGE->url, array('pick' => $pick, 'confirmed' => 1)), $PAGE->url); echo $output->box($sourcecontroller->render_preview($PAGE), 'template-preview-confirm'); echo $output->footer(); die(); } else { require_sesskey(); $targetcontroller->update_definition($sourcecontroller->get_definition_copy($targetcontroller)); $DB->set_field('grading_definitions', 'timecopied', time(), array('id' => $definition->id)); redirect(new moodle_url('/grade/grading/manage.php', array('areaid' => $targetid))); } } // process removing a template if ($remove) { $sourceid = $DB->get_field('grading_definitions', 'areaid', array('id' => $remove), MUST_EXIST); $sourcemanager = get_grading_manager($sourceid); $sourcecontroller = $sourcemanager->get_controller($method); if (!$sourcecontroller->is_shared_template()) { throw new moodle_exception('attempt_to_delete_nontemplate', 'core_grading'); } if (!$sourcecontroller->is_form_defined()) { throw new moodle_exception('form_definition_mismatch', 'core_grading'); } $definition = $sourcecontroller->get_definition(); if ($canmanage or ($canshare and ($definition->usercreated == $USER->id))) { // ok, this user can drop the template } else { throw new moodle_exception('no_permission_to_remove_template', 'core_grading'); } if (!$confirmed) { echo $output->header(); echo $output->confirm(get_string('templatedeleteconfirm', 'core_grading', s($definition->name)), new moodle_url($PAGE->url, array('remove' => $remove, 'confirmed' => 1)), $PAGE->url); echo $output->box($sourcecontroller->render_preview($PAGE), 'template-preview-confirm'); echo $output->footer(); die(); } else { require_sesskey(); $sourcecontroller->delete_definition(); redirect($PAGE->url); } } $searchform = new grading_search_template_form($PAGE->url, null, 'GET', '', array('class' => 'templatesearchform')); if ($searchdata = $searchform->get_data()) { $tokens = grading_manager::tokenize($searchdata->needle); $includeownforms = (!empty($searchdata->mode)); } else { $tokens = array(); $includeownforms = false; } // construct the SQL to find all matching templates $sql = "SELECT DISTINCT gd.id, gd.areaid, gd.name, gd.usercreated FROM {grading_definitions} gd JOIN {grading_areas} ga ON (gd.areaid = ga.id) JOIN {context} cx ON (ga.contextid = cx.id)"; // join method-specific tables from the plugin scope $sql .= $targetcontrollerclass::sql_search_from_tables('gd.id'); $sql .= " WHERE gd.method = ?"; $params = array($method); if (!$includeownforms) { // search for public templates only $sql .= " AND ga.contextid = ? AND ga.component = 'core_grading'"; $params[] = context_system::instance()->id; } else { // search both templates and own forms in other areas $sql .= " AND ((ga.contextid = ? AND ga.component = 'core_grading') OR (gd.usercreated = ? AND gd.status = ?))"; $params = array_merge($params, array(context_system::instance()->id, $USER->id, gradingform_controller::DEFINITION_STATUS_READY)); } if ($tokens) { $subsql = array(); // search for any of the tokens in the definition name foreach ($tokens as $token) { $subsql[] = $DB->sql_like('gd.name', '?', false, false); $params[] = '%'.$DB->sql_like_escape($token).'%'; } // search for any of the tokens in the definition description foreach ($tokens as $token) { $subsql[] = $DB->sql_like('gd.description', '?', false, false); $params[] = '%'.$DB->sql_like_escape($token).'%'; } // search for the needle in method-specific tables foreach ($tokens as $token) { list($methodsql, $methodparams) = $targetcontrollerclass::sql_search_where($token); $subsql = array_merge($subsql, $methodsql); $params = array_merge($params, $methodparams); } $sql .= " AND ((" . join(")\n OR (", $subsql) . "))"; } $sql .= " ORDER BY gd.name"; $rs = $DB->get_recordset_sql($sql, $params); echo $output->header(); $searchform->display(); $found = 0; foreach ($rs as $template) { $found++; $out = ''; $manager = get_grading_manager($template->areaid); $controller = $manager->get_controller($method); if ($controller->is_shared_template()) { $templatetag = html_writer::tag('span', get_string('templatetypeshared', 'core_grading'), array('class' => 'type shared')); $templatesrc = ''; } else if ($controller->is_own_form()) { $templatetag = html_writer::tag('span', get_string('templatetypeown', 'core_grading'), array('class' => 'type ownform')); $templatesrc = get_string('templatesource', 'core_grading', array( 'component' => $manager->get_component_title(), 'area' => $manager->get_area_title())); } else { throw new coding_exception('Something is wrong, the displayed form must be either template or own form'); } $out .= $output->heading(s($template->name).' '.$templatetag, 2, 'template-name'); $out .= $output->container($templatesrc, 'template-source'); $out .= $output->box($controller->render_preview($PAGE), 'template-preview'); $actions = array(); if ($controller->is_shared_template()) { $actions[] = $output->pick_action_icon(new moodle_url($PAGE->url, array('pick' => $template->id)), get_string('templatepick', 'core_grading'), 'i/valid', 'pick template'); if ($canmanage or ($canshare and ($template->usercreated == $USER->id))) { //$actions[] = $output->pick_action_icon(new moodle_url($PAGE->url, array('edit' => $template->id)), // get_string('templateedit', 'core_grading'), 'i/edit', 'edit'); $actions[] = $output->pick_action_icon(new moodle_url($PAGE->url, array('remove' => $template->id)), get_string('templatedelete', 'core_grading'), 't/delete', 'remove'); } } else if ($controller->is_own_form()) { $actions[] = $output->pick_action_icon(new moodle_url($PAGE->url, array('pick' => $template->id)), get_string('templatepickownform', 'core_grading'), 'i/valid', 'pick ownform'); } $out .= $output->box(join(' ', $actions), 'template-actions'); $out .= $output->box($controller->get_formatted_description(), 'template-description'); // ideally we should highlight just the name, description and the fields // in the preview that were actually searched. to make our life easier, we // simply highlight the tokens everywhere they appear, even if that exact // piece was not searched. echo highlight(join(' ', $tokens), $out); } $rs->close(); if (!$found) { echo $output->heading(get_string('nosharedformfound', 'core_grading')); } echo $output->single_button( new moodle_url('/grade/grading/manage.php', array('areaid' => $targetid)), get_string('back'), 'get'); echo $output->footer(); PK'\ ыtests/generator/lib.phpnu[. /** * Generator for the core_grading subsystem generator. * * @package core_grading * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Generator for the core_grading subsystem generator. * * @package core_grading * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_grading_generator extends component_generator_base { /** * Create an instance of an advanced grading area. * * @param context $context * @param string $component * @param string $areaname An area belonging to the specified component * @param string $method An available gradingform type * @return gradingform_controller The controller for the created instance */ public function create_instance(context $context, string $component, string $areaname, string $method): gradingform_controller { require_once(__DIR__ . '/../../lib.php'); $manager = get_grading_manager($context, $component, $areaname); $manager->set_active_method($method); return $manager->get_controller($method); } } PK'\a7PPtests/privacy/provider_test.phpnu[. /** * Privacy tests for core_grading. * * @package core_grading * @category test * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_grading\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; use core_privacy\tests\provider_testcase; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_grading\privacy\provider; /** * Privacy tests for core_grading. * * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { /** @var stdClass User without data. */ protected $user0; /** @var stdClass User with data. */ protected $user1; /** @var stdClass User with data. */ protected $user2; /** @var context context_module of an activity without grading definitions. */ protected $instancecontext0; /** @var context context_module of the activity where the grading definitions are. */ protected $instancecontext1; /** @var context context_module of the activity where the grading definitions are. */ protected $instancecontext2; /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid(): void { global $DB; $this->resetAfterTest(); $this->grading_setup_test_scenario_data(); $this->assertCount(2, $DB->get_records('grading_definitions')); // User1 has created grading definitions for instance1 and instance2. $contextlist = provider::get_contexts_for_userid($this->user1->id); $this->assertCount(2, $contextlist); $this->assertContainsEquals($this->instancecontext1->id, $contextlist->get_contextids()); $this->assertContainsEquals($this->instancecontext2->id, $contextlist->get_contextids()); $this->assertNotContainsEquals($this->instancecontext0->id, $contextlist->get_contextids()); // User2 has only modified grading definitions for instance2. $contextlist = provider::get_contexts_for_userid($this->user2->id); $this->assertCount(1, $contextlist); $this->assertContainsEquals($this->instancecontext2->id, $contextlist->get_contextids()); // User0 hasn't created or modified any grading definition. $contextlist = provider::get_contexts_for_userid($this->user0->id); $this->assertCount(0, $contextlist); } /** * Test retrieval of user ids in a given context. */ public function test_get_users_in_context(): void { $this->resetAfterTest(); $this->grading_setup_test_scenario_data(); // Instance two has one user who created the definitions and another who modified it. $userlist = new \core_privacy\local\request\userlist($this->instancecontext2, 'core_grading'); provider::get_users_in_context($userlist); // Check that we get both. $this->assertCount(2, $userlist->get_userids()); } /** * Export for a user with no grading definitions created or modified will not have any data exported. */ public function test_export_user_data_no_content(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $context = \context_system::instance(); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($user->id, $context, 'core_grading'); $this->assertFalse(writer::with_context($context)->has_any_data()); } /** * Test that data is exported correctly for this plugin. */ public function test_export_user_data(): void { global $DB; $this->resetAfterTest(); $now = time(); $defnameprefix = 'fakename'; $this->grading_setup_test_scenario_data($defnameprefix, $now); $this->assertCount(2, $DB->get_records('grading_definitions')); // Validate exported data: instance1 - user0 has NO data. $this->setUser($this->user0); writer::reset(); $writer = writer::with_context($this->instancecontext1); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($this->user0->id, $this->instancecontext1, 'core_grading'); $data = $writer->get_data([get_string('gradingmethod', 'grading')]); $this->assertEmpty($data); // Validate exported data: instance0 - user1 has NO data. $this->setUser($this->user1); writer::reset(); $writer = writer::with_context($this->instancecontext0); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($this->user1->id, $this->instancecontext0, 'core_grading'); $data = $writer->get_data([get_string('gradingmethod', 'grading')]); $this->assertEmpty($data); // Validate exported data: instance1 - user1 has data (user has created and modified it). writer::reset(); $writer = writer::with_context($this->instancecontext1); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($this->user1->id, $this->instancecontext1, 'core_grading'); $data = $writer->get_data([get_string('gradingmethod', 'grading')]); $this->assertCount(1, $data->definitions); $firstkey = reset($data->definitions); $this->assertNotEmpty($firstkey->name); $this->assertEquals('test_method', $firstkey->method); $this->assertEquals(transform::datetime($now), $firstkey->timecreated); $this->assertEquals($this->user1->id, $firstkey->usercreated); $this->assertEquals($defnameprefix.'1', $firstkey->name); // Validate exported data: instance2 - user1 has data (user has created it). writer::reset(); $writer = writer::with_context($this->instancecontext2); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($this->user1->id, $this->instancecontext2, 'core_grading'); $data = $writer->get_data([get_string('gradingmethod', 'grading')]); $this->assertCount(1, $data->definitions); $firstkey = reset($data->definitions); $this->assertNotEmpty($firstkey->name); $this->assertEquals('test_method', $firstkey->method); $this->assertEquals(transform::datetime($now), $firstkey->timecreated); $this->assertEquals($this->user1->id, $firstkey->usercreated); $this->assertEquals($defnameprefix.'2', $firstkey->name); // Validate exported data: instance1 - user2 has NO data. $this->setUser($this->user2); writer::reset(); $writer = writer::with_context($this->instancecontext1); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($this->user2->id, $this->instancecontext1, 'core_grading'); $data = $writer->get_data([get_string('gradingmethod', 'grading')]); $this->assertEmpty($data); // Validate exported data: instance2 - user2 has data (user has modified it). $this->setUser($this->user2); writer::reset(); $writer = writer::with_context($this->instancecontext2); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($this->user2->id, $this->instancecontext2, 'core_grading'); $data = $writer->get_data([get_string('gradingmethod', 'grading')]); $this->assertCount(1, $data->definitions); } /** * Test for provider::delete_data_for_all_users_in_context(). */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $this->resetAfterTest(); $this->grading_setup_test_scenario_data(); // Before deletion, we should have 2 grading_definitions. $this->assertCount(2, $DB->get_records('grading_definitions')); // Delete data. provider::delete_data_for_all_users_in_context($this->instancecontext0); provider::delete_data_for_all_users_in_context($this->instancecontext1); provider::delete_data_for_all_users_in_context($this->instancecontext2); // Before deletion, we should have same grading_definitions (nothing was deleted). $this->assertCount(2, $DB->get_records('grading_definitions')); } /** * Test for provider::delete_data_for_user(). */ public function test_delete_data_for_user(): void { global $DB; $this->resetAfterTest(); $this->grading_setup_test_scenario_data(); // Before deletion, we should have 2 grading_definitions. $this->assertCount(2, $DB->get_records('grading_definitions')); // Delete data for $user0. $contextlist = provider::get_contexts_for_userid($this->user0->id); $approvedcontextlist = new approved_contextlist( $this->user0, 'core_grading', $contextlist->get_contextids() ); provider::delete_data_for_user($approvedcontextlist); // Delete data for $user1. $contextlist = provider::get_contexts_for_userid($this->user1->id); $approvedcontextlist = new approved_contextlist( $this->user1, 'core_grading', $contextlist->get_contextids() ); provider::delete_data_for_user($approvedcontextlist); // Delete data for $user2. $contextlist = provider::get_contexts_for_userid($this->user2->id); $approvedcontextlist = new approved_contextlist( $this->user2, 'core_grading', $contextlist->get_contextids() ); provider::delete_data_for_user($approvedcontextlist); // Before deletion, we should have same grading_definitions (nothing was deleted). $this->assertCount(2, $DB->get_records('grading_definitions')); } /** * Test exporting user data relating to an item ID. */ public function test_export_item_data(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $module = $this->getDataGenerator()->create_module('assign', ['course' => $course]); $user = $this->getDataGenerator()->create_user(); $guidegenerator = \testing_util::get_data_generator()->get_plugin_generator('gradingform_guide'); $this->setUser($user); $modulecontext = \context_module::instance($module->cmid); $controller = $guidegenerator->get_test_guide($modulecontext); // In the situation of mod_assign this would be the id from assign_grades. $itemid = 1; $instance = $controller->create_instance($user->id, $itemid); $data = $guidegenerator->get_test_form_data( $controller, $itemid, 5, 'This user made several mistakes.', 10, 'This user has two pictures.' ); $instance->update($data); $instanceid = $instance->get_data('id'); provider::export_item_data($modulecontext, $itemid, ['Test']); $data = (array) writer::with_context($modulecontext)->get_data(['Test', 'Marking guide', $instance->get_data('id')]); $this->assertCount(2, $data); $this->assertEquals('This user made several mistakes.', $data['Spelling mistakes']->remark); $this->assertEquals(5, $data['Spelling mistakes']->score); $this->assertEquals('This user has two pictures.', $data['Pictures']->remark); $this->assertEquals(10, $data['Pictures']->score); } /** * Test deleting user data related to a context and item ID. */ public function test_delete_instance_data(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $module = $this->getDataGenerator()->create_module('assign', ['course' => $course]); $user = $this->getDataGenerator()->create_user(); $guidegenerator = \testing_util::get_data_generator()->get_plugin_generator('gradingform_guide'); $this->setUser($user); $modulecontext = \context_module::instance($module->cmid); $controller = $guidegenerator->get_test_guide($modulecontext); // In the situation of mod_assign this would be the id from assign_grades. $itemid = 1; $instance = $controller->create_instance($user->id, $itemid); $data = $guidegenerator->get_test_form_data( $controller, $itemid, 5, 'This user made several mistakes.', 10, 'This user has two pictures.' ); $instance->update($data); $itemid = 2; $instance = $controller->create_instance($user->id, $itemid); $data = $guidegenerator->get_test_form_data( $controller, $itemid, 25, 'This user made no mistakes.', 5, 'This user has one picture.' ); $instance->update($data); // Check how many records we have in the fillings table. $records = $DB->get_records('gradingform_guide_fillings'); $this->assertCount(4, $records); // Let's delete one of the instances (the last one would be the easiest). provider::delete_instance_data($modulecontext, $itemid); $records = $DB->get_records('gradingform_guide_fillings'); $this->assertCount(2, $records); foreach ($records as $record) { $this->assertNotEquals($instance->get_id(), $record->instanceid); } // This will delete all the rest of the instances for this context. provider::delete_instance_data($modulecontext); $records = $DB->get_records('gradingform_guide_fillings'); $this->assertEmpty($records); } /** * Test the deletion of multiple instances at once. */ public function test_delete_data_for_instances(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $module = $this->getDataGenerator()->create_module('assign', ['course' => $course]); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $guidegenerator = \testing_util::get_data_generator()->get_plugin_generator('gradingform_guide'); $this->setUser($user1); $modulecontext = \context_module::instance($module->cmid); $controller = $guidegenerator->get_test_guide($modulecontext); // In the situation of mod_assign this would be the id from assign_grades. $itemid1 = 1; $instance1 = $controller->create_instance($user1->id, $itemid1); $data = $guidegenerator->get_test_form_data( $controller, $itemid1, 5, 'This user made several mistakes.', 10, 'This user has two pictures.' ); $instance1->update($data); $itemid2 = 2; $instance2 = $controller->create_instance($user2->id, $itemid2); $data = $guidegenerator->get_test_form_data( $controller, $itemid2, 15, 'This user made a couple of mistakes.', 10, 'This user has one picture.' ); $instance2->update($data); $itemid3 = 3; $instance3 = $controller->create_instance($user3->id, $itemid3); $data = $guidegenerator->get_test_form_data( $controller, $itemid3, 20, 'This user made one mistakes.', 10, 'This user has one picture.' ); $instance3->update($data); $records = $DB->get_records('gradingform_guide_fillings'); $this->assertCount(6, $records); // Delete all user data for items 1 and 3. provider::delete_data_for_instances($modulecontext, [$itemid1, $itemid3]); $records = $DB->get_records('gradingform_guide_fillings'); $this->assertCount(2, $records); $instanceid = $instance2->get_data('id'); // The instance id should match for all remaining records. foreach ($records as $record) { $this->assertEquals($instanceid, $record->instanceid); } } /** * Helper function to setup the environment. * * course * | * +--instance0 (assignment) * | | * +--instance1 (assignment) * | | * | +--grading_definition1 (created and modified by user1) * | | * +--instance2 (assignment) * | | * | +--grading_definition2 (created by user1 and modified by user2) * * * user0 hasn't any data. * * @param string $defnameprefix * @param timestamp $now */ protected function grading_setup_test_scenario_data($defnameprefix = null, $now = null) { global $DB; $this->user0 = $this->getDataGenerator()->create_user(); $this->user1 = $this->getDataGenerator()->create_user(); $this->user2 = $this->getDataGenerator()->create_user(); // Create a course. $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); // Create some assignment instances. $params = (object)array( 'course' => $course->id, 'name' => 'Testing instance' ); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $instance0 = $generator->create_instance($params); $cm0 = get_coursemodule_from_instance('assign', $instance0->id); $this->instancecontext0 = \context_module::instance($cm0->id); $instance1 = $generator->create_instance($params); $cm1 = get_coursemodule_from_instance('assign', $instance1->id); $this->instancecontext1 = \context_module::instance($cm1->id); $instance2 = $generator->create_instance($params); $cm2 = get_coursemodule_from_instance('assign', $instance2->id); $this->instancecontext2 = \context_module::instance($cm2->id); // Create fake grading areas. $fakearea1 = (object)array( 'contextid' => $this->instancecontext1->id, 'component' => 'mod_assign', 'areaname' => 'submissions', 'activemethod' => 'test_method' ); $fakeareaid1 = $DB->insert_record('grading_areas', $fakearea1); $fakearea2 = clone($fakearea1); $fakearea2->contextid = $this->instancecontext2->id; $fakeareaid2 = $DB->insert_record('grading_areas', $fakearea2); // Create fake grading definitions. if (empty($now)) { $now = time(); } if (empty($defnameprefix)) { $defnameprefix = 'fakename'; } $fakedefinition1 = (object)array( 'areaid' => $fakeareaid1, 'method' => 'test_method', 'name' => $defnameprefix.'1', 'status' => 0, 'timecreated' => $now, 'usercreated' => $this->user1->id, 'timemodified' => $now + 1, 'usermodified' => $this->user1->id, ); $fakedefid1 = $DB->insert_record('grading_definitions', $fakedefinition1); $fakedefinition2 = clone($fakedefinition1); $fakedefinition2->areaid = $fakeareaid2; $fakedefinition2->name = $defnameprefix.'2'; $fakedefinition2->usermodified = $this->user2->id; $fakedefid2 = $DB->insert_record('grading_definitions', $fakedefinition2); } } PK'\ II&tests/privacy/legacy_polyfill_test.phpnu[. namespace core_grading\privacy; defined('MOODLE_INTERNAL') || die(); /** * Unit tests for the Grading API's privacy legacy_polyfill. * * @package core_grading * @category test * @copyright 2018 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class legacy_polyfill_test extends \advanced_testcase { /** * Test that the core_grading\privacy\legacy_polyfill works and that the static _export_gradingform_instance_data can be called. */ public function test_export_gradingform_instance_data(): void { $context = \context_system::instance(); $mock = $this->createMock(test_gradingform_legacy_polyfill_mock_wrapper::class); $mock->expects($this->once()) ->method('get_return_value') ->with('_export_gradingform_instance_data', [$context, 3, ['subcontext']]); test_legacy_polyfill_gradingform_provider::$mock = $mock; test_legacy_polyfill_gradingform_provider::export_gradingform_instance_data($context, 3, ['subcontext']); } /** * Test for _get_metadata shim. */ public function test_get_metadata(): void { $collection = new \core_privacy\local\metadata\collection('core_gradingform'); $this->assertSame($collection, test_legacy_polyfill_gradingform_provider::get_metadata($collection)); } /** * Test the _delete_gradingform_for_instances shim. */ public function test_delete_gradingform_for_instances(): void { $context = \context_system::instance(); $mock = $this->createMock(test_gradingform_legacy_polyfill_mock_wrapper::class); $mock->expects($this->once()) ->method('get_return_value') ->with('_delete_gradingform_for_instances', [[3, 17]]); test_legacy_polyfill_gradingform_provider::$mock = $mock; test_legacy_polyfill_gradingform_provider::delete_gradingform_for_instances([3, 17]); } } /** * Legacy polyfill test class for the gradingform_provider. * * @copyright 2018 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_legacy_polyfill_gradingform_provider implements \core_privacy\local\metadata\provider, \core_grading\privacy\gradingform_provider_v2 { use \core_grading\privacy\gradingform_legacy_polyfill; use \core_privacy\local\legacy_polyfill; /** * @var test_legacy_polyfill_gradingform_provider $mock. */ public static $mock = null; /** * Export user data relating to an instance ID. * * @param \context $context Context to use with the export writer. * @param int $instanceid The instance ID to export data for. * @param array $subcontext The directory to export this data to. */ protected static function _export_gradingform_instance_data(\context $context, $instanceid, $subcontext) { static::$mock->get_return_value(__FUNCTION__, func_get_args()); } /** * Deletes all user data related to the provided instance IDs. * * @param array $instanceids The instance IDs to delete information from. */ protected static function _delete_gradingform_for_instances($instanceids) { static::$mock->get_return_value(__FUNCTION__, func_get_args()); } /** * Returns metadata about this plugin. * * @param \core_privacy\local\metadata\collection $collection The initialised collection to add items to. * @return \core_privacy\local\metadata\collection A listing of user data stored through this system. */ protected static function _get_metadata(\core_privacy\local\metadata\collection $collection) { return $collection; } } /** * Called inside the polyfill methods in the test polyfill provider, allowing us to ensure these are called with correct params. * * @copyright 2018 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_gradingform_legacy_polyfill_mock_wrapper { /** * Get the return value for the specified item. */ public function get_return_value() { } } PK'\a؞JJtests/grading_manager_test.phpnu[. namespace core_grading; use grading_manager; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/grade/grading/lib.php'); // Include the code to test /** * Test cases for the grading manager API * * @package core_grading * @category test * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class grading_manager_test extends \advanced_testcase { public function test_basic_instantiation(): void { $manager1 = get_grading_manager(); $fakecontext = (object)array( 'id' => 42, 'contextlevel' => CONTEXT_MODULE, 'instanceid' => 22, 'path' => '/1/3/15/42', 'depth' => 4); $manager2 = get_grading_manager($fakecontext); $manager3 = get_grading_manager($fakecontext, 'assignment_upload'); $manager4 = get_grading_manager($fakecontext, 'assignment_upload', 'submission'); } /** * Unit test to set and get grading areas */ public function test_set_and_get_grading_area(): void { global $DB; $this->resetAfterTest(true); //sleep(2); // to make sure the microtime will always return unique values // No sleeping in tests!!! --skodak $areaname1 = 'area1-' . (string)microtime(true); $areaname2 = 'area2-' . (string)microtime(true); $fakecontext = (object)array( 'id' => 42, 'contextlevel' => CONTEXT_MODULE, 'instanceid' => 22, 'path' => '/1/3/15/42', 'depth' => 4); // non-existing area $gradingman = get_grading_manager($fakecontext, 'mod_foobar', $areaname1); $this->assertNull($gradingman->get_active_method()); // creates area implicitly and sets active method $this->assertTrue($gradingman->set_active_method('rubric')); $this->assertEquals('rubric', $gradingman->get_active_method()); // repeat setting of already set active method $this->assertFalse($gradingman->set_active_method('rubric')); // switch the manager to another area $gradingman->set_area($areaname2); $this->assertNull($gradingman->get_active_method()); // switch back and ask again $gradingman->set_area($areaname1); $this->assertEquals('rubric', $gradingman->get_active_method()); // attempting to set an invalid method $this->expectException(\moodle_exception::class); $gradingman->set_active_method('no_one_should_ever_try_to_implement_a_method_with_this_silly_name'); } /** * Unit test to check the tokenize method */ public function test_tokenize(): void { $UTFfailuremessage = 'A test using UTF-8 characters has failed. Consider updating PHP and PHP\'s PCRE or INTL extensions (MDL-30494)'; $needle = " šašek, \n\n \r a král; \t"; $tokens = grading_manager::tokenize($needle); $this->assertEquals(2, count($tokens), $UTFfailuremessage); $this->assertTrue(in_array('šašek', $tokens), $UTFfailuremessage); $this->assertTrue(in_array('král', $tokens), $UTFfailuremessage); $needle = ' " šašek a král " '; $tokens = grading_manager::tokenize($needle); $this->assertEquals(1, count($tokens)); $this->assertTrue(in_array('šašek a král', $tokens)); $needle = '""'; $tokens = grading_manager::tokenize($needle); $this->assertTrue(empty($tokens)); $needle = '"0"'; $tokens = grading_manager::tokenize($needle); $this->assertEquals(1, count($tokens)); $this->assertTrue(in_array('0', $tokens)); $needle = 'Aha, then who\'s a bad guy here he?'; $tokens = grading_manager::tokenize($needle); $this->assertEquals(8, count($tokens)); $this->assertTrue(in_array('span', $tokens)); // Extracted the tag name $this->assertTrue(in_array('Aha', $tokens)); $this->assertTrue(in_array('who', $tokens)); // Removed the trailing 's $this->assertTrue(!in_array('a', $tokens)); //Single letter token was dropped $this->assertTrue(in_array('he', $tokens)); // Removed the trailing ? $needle = 'grammar, "english language"'; $tokens = grading_manager::tokenize($needle); $this->assertTrue(in_array('grammar', $tokens)); $this->assertTrue(in_array('english', $tokens)); $this->assertTrue(in_array('language', $tokens)); $this->assertTrue(!in_array('english language', $tokens)); // Quoting part of the string is not supported } } PK'\ЋL L tests/generator_test.phpnu[. /** * Generator testcase for the gradingforum_rubric generator. * * @package core_grading * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_grading; use advanced_testcase; use context_module; use gradingform_controller; use gradingform_rubric_controller; /** * Generator testcase for the core_grading generator. * * @package core_grading * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class generator_test extends advanced_testcase { /** * Test gradingform controller creation. */ public function test_create_instance(): void { $this->resetAfterTest(true); // Fetch generators. $generator = \testing_util::get_data_generator(); $gradinggenerator = $generator->get_plugin_generator('core_grading'); // Create items required for testing. $course = $generator->create_course(); $module = $generator->create_module('assign', ['course' => $course]); $user = $generator->create_user(); $context = context_module::instance($module->cmid); // The assignment module has an itemumber 0 which is an advanced grading area called 'submissions'. $component = 'mod_assign'; $area = 'submissions'; $controller = $gradinggenerator->create_instance($context, $component, $area, 'rubric'); // This should be a rubric. $this->assertInstanceOf(gradingform_controller::class, $controller); $this->assertInstanceOf(gradingform_rubric_controller::class, $controller); $this->assertEquals($context, $controller->get_context()); $this->assertEquals($component, $controller->get_component()); $this->assertEquals($area, $controller->get_area()); } } PK'\p,tests/behat/behat_grading.phpnu[. /** * Grading methods steps definitions. * * @package core_grading * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); use Behat\Gherkin\Node\TableNode as TableNode; /** * Generic grading methods step definitions. * * @package core_grading * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_grading extends behat_base { /** * Goes to the selected advanced grading page. You should be in the course page when this step begins. * * @Given /^I go to "(?P(?:[^"]|\\")*)" advanced grading page$/ * @param string $activityname */ public function i_go_to_advanced_grading_page($activityname) { try { $this->execute("behat_general::i_click_on_in_the", [$this->escape($activityname), 'link', 'page', 'region']); } catch (Exception $e) { $this->execute('behat_navigation::go_to_breadcrumb_location', $this->escape($activityname)); } $this->execute('behat_navigation::i_navigate_to_in_current_page_administration', get_string('gradingmanagement', 'grading')); } /** * Goes to the selected advanced grading definition page. You should be in the course page when this step begins. * * @Given /^I go to "(?P(?:[^"]|\\")*)" advanced grading definition page$/ * @param string $activityname */ public function i_go_to_advanced_grading_definition_page($activityname) { // Transforming to literals, probably not necessary, just in case. $newactionliteral = behat_context_helper::escape(get_string("manageactionnew", "grading")); $editactionliteral = behat_context_helper::escape(get_string("manageactionedit", "grading")); // Working both when adding and editing. $definitionxpath = "//a[@class='action btn btn-lg']" . "[./descendant::*[contains(., $newactionliteral) or contains(., $editactionliteral)]]"; $this->execute('behat_grading::i_go_to_advanced_grading_page', $this->escape($activityname)); $this->execute("behat_general::i_click_on", array($this->escape($definitionxpath), "xpath_element")); } /** * Goes to the student's advanced grading page. * * @Given /^I go to "(?P(?:[^"]|\\")*)" "(?P(?:[^"]|\\")*)" activity advanced grading page$/ * @param string $userfullname The user full name including firstname and lastname. * @param string $activityname The activity name */ public function i_go_to_activity_advanced_grading_page($userfullname, $activityname) { // Step to access the user grade page from the grading page. $this->execute('behat_navigation::go_to_breadcrumb_location', $this->escape($activityname)); $this->execute('behat_general::click_link', get_string('gradeitem:submissions', 'mod_assign')); $this->execute('behat_general::i_click_on_in_the', array( $this->escape(get_string('gradeactions', 'assign')), 'actionmenu', $this->escape($userfullname), 'table_row' )); $this->execute('behat_action_menu::i_choose_in_the_open_action_menu', get_string('gradeverb')); } /** * Publishes current activity grading defined form as a public template. * * @Given /^I publish "(?P(?:[^"]|\\")*)" grading form definition as a public template$/ * @param string $activityname */ public function i_publish_grading_form_definition_as_a_public_template($activityname) { $this->execute('behat_grading::i_go_to_advanced_grading_page', $this->escape($activityname)); $this->execute("behat_general::i_click_on", array($this->escape(get_string("manageactionshare", "grading")), "link")); $this->execute('behat_forms::press_button', get_string('continue')); } /** * Sets a previously created grading form as the activity grading form. * * @Given /^I set "(?P(?:[^"]|\\")*)" activity to use "(?P(?:[^"]|\\")*)" grading form$/ * @param string $activityname * @param string $templatename */ public function i_set_activity_to_use_grading_form($activityname, $templatename) { $templateliteral = behat_context_helper::escape($templatename); $templatexpath = "//h2[@class='template-name'][contains(., $templateliteral)]/" . "following-sibling::div[contains(concat(' ', normalize-space(@class), ' '), ' template-actions ')]"; // Should work with both templates and own forms. $literaltemplate = behat_context_helper::escape(get_string('templatepick', 'grading')); $literalownform = behat_context_helper::escape(get_string('templatepickownform', 'grading')); $usetemplatexpath = "/a[./descendant::div[text()=$literaltemplate]]|" . "/a[./descendant::div[text()=$literalownform]]"; $this->execute('behat_grading::i_go_to_advanced_grading_page', $this->escape($activityname)); $this->execute('behat_general::click_link', $this->escape(get_string('manageactionclone', 'grading'))); $this->execute('behat_forms::i_set_the_field_to', array(get_string('searchownforms', 'grading'), 1)); $this->execute('behat_general::i_click_on_in_the', array(get_string('search'), "button", "region-main", "region") ); $this->execute('behat_general::i_click_on_in_the', array($this->escape($usetemplatexpath), "xpath_element", $this->escape($templatexpath), "xpath_element") ); $this->execute('behat_forms::press_button', get_string('continue')); } /** * Saves the current page advanced grading form. * * @When /^I save the advanced grading form$/ */ public function i_save_the_advanced_grading_form() { $this->execute('behat_forms::press_button', get_string('savechanges')); $this->execute('behat_general::i_click_on', array($this->escape(get_string('editsettings')), 'link')); $this->execute('behat_forms::press_button', get_string('cancel')); $this->execute('behat_general::click_link', get_string('gradeitem:submissions', 'mod_assign')); } /** * Grades an activity using advanced grading. Note the grade is set by other steps, depending on the grading method. * * @Given /^I complete the advanced grading form with these values:$/ * @param TableNode $data */ public function i_complete_the_advanced_grading_form_with_these_values(TableNode $data) { $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data); $this->execute('behat_grading::i_save_the_advanced_grading_form'); } } PK'\*K pick_form.phpnu[. /** * Defines forms used by pick.php * * @package core_grading * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/lib/formslib.php'); /** * Allows to search for a specific shared template * * @package core_grading * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grading_search_template_form extends moodleform { /** * Pretty simple search box */ public function definition() { $mform = $this->_form; $mform->addElement('header', 'searchheader', get_string('searchtemplate', 'core_grading')); $mform->addHelpButton('searchheader', 'searchtemplate', 'core_grading'); $mform->addGroup(array( $mform->createElement('checkbox', 'mode', '', get_string('searchownforms', 'core_grading')), $mform->createElement('text', 'needle', '', array('size' => 30)), $mform->createElement('submit', 'submitbutton', get_string('search')), ), 'buttonar', '', array(' '), false); $mform->setType('needle', PARAM_TEXT); $mform->setType('buttonar', PARAM_RAW); } } PK\0Ł message.min.js.mapnu[{"version":3,"file":"message.min.js","sources":["../../../src/bulkactions/grading/message.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Bulk action for messaging users in the assignment grading page.\n *\n * @module mod_assign/bulkactions/grading/message\n * @copyright 2024 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BulkAction from 'core/bulkactions/bulk_action';\nimport Templates from 'core/templates';\nimport {showModal as showMessageModal} from 'core_message/message_send_bulk';\n\nconst Selectors = {\n selectBulkItemCheckbox: 'input[type=\"checkbox\"][name=\"selectedusers\"]:checked',\n};\n\nexport default class extends BulkAction {\n getBulkActionTriggerSelector() {\n return '[data-type=\"bulkactions\"] [data-action=\"message\"]';\n }\n\n triggerBulkAction() {\n const selectedUsers = [...document.querySelectorAll(Selectors.selectBulkItemCheckbox)].map(checkbox => checkbox.value);\n showMessageModal(selectedUsers);\n }\n\n async renderBulkActionTrigger(showInDropdown, index) {\n return Templates.render('mod_assign/bulkactions/grading/bulk_message_trigger', {\n showindropdown: showInDropdown,\n isfirst: index === 0,\n });\n }\n}\n"],"names":["Selectors","BulkAction","getBulkActionTriggerSelector","triggerBulkAction","selectedUsers","document","querySelectorAll","map","checkbox","value","showInDropdown","index","Templates","render","showindropdown","isfirst"],"mappings":";;;;;;;2LA2BMA,iCACsB,8EAGCC,qBACzBC,qCACW,oDAGXC,0BACUC,cAAgB,IAAIC,SAASC,iBAAiBN,mCAAmCO,KAAIC,UAAYA,SAASC,yCAC/FL,6CAGSM,eAAgBC,cACnCC,mBAAUC,OAAO,sDAAuD,CAC3EC,eAAgBJ,eAChBK,QAAmB,IAAVJ"}PK\ эddgeneral_action.min.jsnu[define("mod_assign/bulkactions/grading/general_action",["exports","core/bulkactions/bulk_action","core/templates","core/modal_save_cancel","core/modal_events"],(function(_exports,_bulk_action,_templates,_modal_save_cancel,_modal_events){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classPrivateFieldInitSpec(obj,privateMap,value){!function(obj,privateCollection){if(privateCollection.has(obj))throw new TypeError("Cannot initialize the same private elements twice on an object")}(obj,privateMap),privateMap.set(obj,value)}function _classPrivateFieldGet(receiver,privateMap){return function(receiver,descriptor){if(descriptor.get)return descriptor.get.call(receiver);return descriptor.value}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"get"))}function _classPrivateFieldSet(receiver,privateMap,value){return function(receiver,descriptor,value){if(descriptor.set)descriptor.set.call(receiver,value);else{if(!descriptor.writable)throw new TypeError("attempted to set read only private field");descriptor.value=value}}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"set"),value),value}function _classExtractFieldDescriptor(receiver,privateMap,action){if(!privateMap.has(receiver))throw new TypeError("attempted to "+action+" private field on non-instance");return privateMap.get(receiver)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_bulk_action=_interopRequireDefault(_bulk_action),_templates=_interopRequireDefault(_templates),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_modal_events=_interopRequireDefault(_modal_events);const Selectors_selectBulkItemCheckbox='input[type="checkbox"][name="selectedusers"]:checked';var _cmid=new WeakMap,_buttonIcon=new WeakMap,_buttonLabel=new WeakMap,_confirmationTitle=new WeakMap,_confirmationQuestion=new WeakMap,_confirmationYes=new WeakMap,_sesskey=new WeakMap;class _default extends _bulk_action.default{constructor(cmid,sesskey,actionKey,buttonLabel,buttonIcon,confirmationTitle,confirmationQuestion,confirmationYes){var obj,key,value;super(),value=void 0,(key="actionKey")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,_classPrivateFieldInitSpec(this,_cmid,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_buttonIcon,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_buttonLabel,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_confirmationTitle,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_confirmationQuestion,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_confirmationYes,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_sesskey,{writable:!0,value:void 0}),_classPrivateFieldSet(this,_cmid,cmid),_classPrivateFieldSet(this,_sesskey,sesskey),this.actionKey=actionKey,_classPrivateFieldSet(this,_buttonLabel,buttonLabel),_classPrivateFieldSet(this,_buttonIcon,buttonIcon),_classPrivateFieldSet(this,_confirmationTitle,confirmationTitle),_classPrivateFieldSet(this,_confirmationQuestion,confirmationQuestion),_classPrivateFieldSet(this,_confirmationYes,confirmationYes)}getBulkActionTriggerSelector(){return'[data-type="bulkactions"] [data-action="'.concat(this.actionKey,'"]')}async triggerBulkAction(){const selectedUsers=[...document.querySelectorAll(Selectors_selectBulkItemCheckbox)].map((checkbox=>checkbox.value)),modal=await _modal_save_cancel.default.create({title:await _classPrivateFieldGet(this,_confirmationTitle),buttons:{save:await _classPrivateFieldGet(this,_confirmationYes)},body:_templates.default.render("mod_assign/bulkactions/grading/bulk_action_modal_body",{text:await _classPrivateFieldGet(this,_confirmationQuestion),operation:this.actionKey,cmid:_classPrivateFieldGet(this,_cmid),selectedusers:selectedUsers.join(","),sesskey:_classPrivateFieldGet(this,_sesskey)}),show:!0,removeOnClose:!0});modal.getRoot().on(_modal_events.default.save,(()=>{modal.getRoot().find("form").submit()}))}async renderBulkActionTrigger(showInDropdown,index){return _templates.default.render("mod_assign/bulkactions/grading/bulk_general_action_trigger",{action:this.actionKey,title:await _classPrivateFieldGet(this,_buttonLabel),icon:await _classPrivateFieldGet(this,_buttonIcon),showindropdown:showInDropdown,isfirst:0===index})}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=general_action.min.js.mapPK\sS}  setmarkingallocation.min.jsnu[define("mod_assign/bulkactions/grading/setmarkingallocation",["exports","core/bulkactions/bulk_action","core/templates","core/str","core/modal_save_cancel","core/modal_events"],(function(_exports,_bulk_action,_templates,_str,_modal_save_cancel,_modal_events){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classPrivateFieldInitSpec(obj,privateMap,value){!function(obj,privateCollection){if(privateCollection.has(obj))throw new TypeError("Cannot initialize the same private elements twice on an object")}(obj,privateMap),privateMap.set(obj,value)}function _classPrivateFieldGet(receiver,privateMap){return function(receiver,descriptor){if(descriptor.get)return descriptor.get.call(receiver);return descriptor.value}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"get"))}function _classPrivateFieldSet(receiver,privateMap,value){return function(receiver,descriptor,value){if(descriptor.set)descriptor.set.call(receiver,value);else{if(!descriptor.writable)throw new TypeError("attempted to set read only private field");descriptor.value=value}}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"set"),value),value}function _classExtractFieldDescriptor(receiver,privateMap,action){if(!privateMap.has(receiver))throw new TypeError("attempted to "+action+" private field on non-instance");return privateMap.get(receiver)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_bulk_action=_interopRequireDefault(_bulk_action),_templates=_interopRequireDefault(_templates),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_modal_events=_interopRequireDefault(_modal_events);const Selectors_selectBulkItemCheckbox='input[type="checkbox"][name="selectedusers"]:checked';var _cmid=new WeakMap,_sesskey=new WeakMap;class _default extends _bulk_action.default{constructor(cmid,sesskey){super(),_classPrivateFieldInitSpec(this,_cmid,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_sesskey,{writable:!0,value:void 0}),_classPrivateFieldSet(this,_cmid,cmid),_classPrivateFieldSet(this,_sesskey,sesskey)}getBulkActionTriggerSelector(){return'[data-type="bulkactions"] [data-action="setmarkingallocation"]'}async triggerBulkAction(){const selectedUsers=[...document.querySelectorAll(Selectors_selectBulkItemCheckbox)].map((checkbox=>checkbox.value)),modal=await _modal_save_cancel.default.create({title:await(0,_str.getString)("setmarkingallocation","mod_assign"),buttons:{save:await(0,_str.getString)("batchoperationsetmarkingallocation","mod_assign")},body:_templates.default.render("mod_assign/bulkactions/grading/bulk_action_modal_body",{text:await(0,_str.getString)("batchoperationconfirmsetmarkingallocation","mod_assign"),operation:"setmarkingallocation",cmid:_classPrivateFieldGet(this,_cmid),selectedusers:selectedUsers.join(","),sesskey:_classPrivateFieldGet(this,_sesskey)}),show:!0,removeOnClose:!0});modal.getRoot().on(_modal_events.default.save,(e=>{e.preventDefault(),modal.getRoot().find("form").submit()}))}async renderBulkActionTrigger(showInDropdown,index){return _templates.default.render("mod_assign/bulkactions/grading/bulk_setmarkingallocation_trigger",{showindropdown:showInDropdown,isfirst:0===index})}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=setmarkingallocation.min.js.mapPK\[  setmarkingworkflowstate.min.jsnu[define("mod_assign/bulkactions/grading/setmarkingworkflowstate",["exports","core/bulkactions/bulk_action","core/templates","core/str","core/modal_save_cancel","core/modal_events"],(function(_exports,_bulk_action,_templates,_str,_modal_save_cancel,_modal_events){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classPrivateFieldInitSpec(obj,privateMap,value){!function(obj,privateCollection){if(privateCollection.has(obj))throw new TypeError("Cannot initialize the same private elements twice on an object")}(obj,privateMap),privateMap.set(obj,value)}function _classPrivateFieldGet(receiver,privateMap){return function(receiver,descriptor){if(descriptor.get)return descriptor.get.call(receiver);return descriptor.value}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"get"))}function _classPrivateFieldSet(receiver,privateMap,value){return function(receiver,descriptor,value){if(descriptor.set)descriptor.set.call(receiver,value);else{if(!descriptor.writable)throw new TypeError("attempted to set read only private field");descriptor.value=value}}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"set"),value),value}function _classExtractFieldDescriptor(receiver,privateMap,action){if(!privateMap.has(receiver))throw new TypeError("attempted to "+action+" private field on non-instance");return privateMap.get(receiver)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_bulk_action=_interopRequireDefault(_bulk_action),_templates=_interopRequireDefault(_templates),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_modal_events=_interopRequireDefault(_modal_events);const Selectors_selectBulkItemCheckbox='input[type="checkbox"][name="selectedusers"]:checked';var _cmid=new WeakMap,_sesskey=new WeakMap;class _default extends _bulk_action.default{constructor(cmid,sesskey){super(),_classPrivateFieldInitSpec(this,_cmid,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_sesskey,{writable:!0,value:void 0}),_classPrivateFieldSet(this,_cmid,cmid),_classPrivateFieldSet(this,_sesskey,sesskey)}getBulkActionTriggerSelector(){return'[data-type="bulkactions"] [data-action="setmarkingworkflowstate"]'}async triggerBulkAction(){const selectedUsers=[...document.querySelectorAll(Selectors_selectBulkItemCheckbox)].map((checkbox=>checkbox.value)),modal=await _modal_save_cancel.default.create({title:await(0,_str.getString)("setmarkingworkflowstate","mod_assign"),buttons:{save:await(0,_str.getString)("batchoperationsetmarkingworkflowstate","mod_assign")},body:_templates.default.render("mod_assign/bulkactions/grading/bulk_action_modal_body",{text:await(0,_str.getString)("batchoperationconfirmsetmarkingworkflowstate","mod_assign"),operation:"setmarkingworkflowstate",cmid:_classPrivateFieldGet(this,_cmid),selectedusers:selectedUsers.join(","),sesskey:_classPrivateFieldGet(this,_sesskey)}),show:!0,removeOnClose:!0});modal.getRoot().on(_modal_events.default.save,(e=>{e.preventDefault(),modal.getRoot().find("form").submit()}))}async renderBulkActionTrigger(showInDropdown,index){return _templates.default.render("mod_assign/bulkactions/grading/bulk_setmarkingworkflowstate_trigger",{showindropdown:showInDropdown,isfirst:0===index})}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=setmarkingworkflowstate.min.js.mapPK\ W**setmarkingallocation.min.js.mapnu[{"version":3,"file":"setmarkingallocation.min.js","sources":["../../../src/bulkactions/grading/setmarkingallocation.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Bulk action for allocating markers in the assignment grading page.\n *\n * @module mod_assign/bulkactions/grading/setmarkingallocation\n * @copyright 2024 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BulkAction from 'core/bulkactions/bulk_action';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport SaveCancelModal from 'core/modal_save_cancel';\nimport ModalEvents from 'core/modal_events';\n\nconst Selectors = {\n selectBulkItemCheckbox: 'input[type=\"checkbox\"][name=\"selectedusers\"]:checked',\n};\n\nexport default class extends BulkAction {\n /** @type {number} The course module ID. */\n #cmid;\n\n /** @type {string} The session key. */\n #sesskey;\n\n /**\n * The class constructor.\n *\n * @param {number} cmid The course module ID.\n * @param {string} sesskey The session key.\n */\n constructor(cmid, sesskey) {\n super();\n this.#cmid = cmid;\n this.#sesskey = sesskey;\n }\n\n getBulkActionTriggerSelector() {\n return '[data-type=\"bulkactions\"] [data-action=\"setmarkingallocation\"]';\n }\n\n async triggerBulkAction() {\n const selectedUsers = [...document.querySelectorAll(Selectors.selectBulkItemCheckbox)].map(checkbox => checkbox.value);\n\n const modal = await SaveCancelModal.create({\n title: await getString('setmarkingallocation', 'mod_assign'),\n buttons: {\n save: await getString('batchoperationsetmarkingallocation', 'mod_assign'),\n },\n body: Templates.render('mod_assign/bulkactions/grading/bulk_action_modal_body', {\n text: await getString('batchoperationconfirmsetmarkingallocation', 'mod_assign'),\n operation: 'setmarkingallocation',\n cmid: this.#cmid,\n selectedusers: selectedUsers.join(','),\n sesskey: this.#sesskey\n }),\n show: true,\n removeOnClose: true,\n });\n\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, (e) => {\n e.preventDefault();\n modal.getRoot().find('form').submit();\n });\n }\n\n async renderBulkActionTrigger(showInDropdown, index) {\n return Templates.render('mod_assign/bulkactions/grading/bulk_setmarkingallocation_trigger', {\n showindropdown: showInDropdown,\n isfirst: index === 0,\n });\n }\n}\n"],"names":["Selectors","BulkAction","constructor","cmid","sesskey","getBulkActionTriggerSelector","selectedUsers","document","querySelectorAll","map","checkbox","value","modal","SaveCancelModal","create","title","buttons","save","body","Templates","render","text","operation","this","selectedusers","join","show","removeOnClose","getRoot","on","ModalEvents","e","preventDefault","find","submit","showInDropdown","index","showindropdown","isfirst"],"mappings":"6oDA6BMA,iCACsB,yHAGCC,qBAazBC,YAAYC,KAAMC,yLAEDD,0CACGC,SAGpBC,qCACW,iGAIDC,cAAgB,IAAIC,SAASC,iBAAiBR,mCAAmCS,KAAIC,UAAYA,SAASC,QAE1GC,YAAcC,2BAAgBC,OAAO,CACvCC,YAAa,kBAAU,uBAAwB,cAC/CC,QAAS,CACLC,WAAY,kBAAU,qCAAsC,eAEhEC,KAAMC,mBAAUC,OAAO,wDAAyD,CAC5EC,WAAY,kBAAU,4CAA6C,cACnEC,UAAW,uBACXnB,2BAAMoB,YACNC,cAAelB,cAAcmB,KAAK,KAClCrB,8BAASmB,iBAEbG,MAAM,EACNC,eAAe,IAInBf,MAAMgB,UAAUC,GAAGC,sBAAYb,MAAOc,IAClCA,EAAEC,iBACFpB,MAAMgB,UAAUK,KAAK,QAAQC,0CAIPC,eAAgBC,cACnCjB,mBAAUC,OAAO,mEAAoE,CACxFiB,eAAgBF,eAChBG,QAAmB,IAAVF"}PK\]//bulk_actions.min.js.mapnu[{"version":3,"file":"bulk_actions.min.js","sources":["../../../src/bulkactions/grading/bulk_actions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Class for defining the bulk actions area in the assignment grading page.\n *\n * @module mod_assign/bulkactions/grading/bulk_actions\n * @copyright 2024 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BulkActions from 'core/bulkactions/bulk_actions';\nimport GeneralAction from './general_action';\nimport DeleteAction from './delete';\nimport ExtendAction from './extend';\nimport MessageAction from './message';\nimport SetMarkingAllocationAction from './setmarkingallocation';\nimport SetMarkingWorkflowStateAction from './setmarkingworkflowstate';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\n\nconst Selectors = {\n selectBulkItemCheckbox: 'input[type=\"checkbox\"][name=\"selectedusers\"]',\n selectBulkItemTrigger: 'input[type=\"checkbox\"][name=\"selectedusers\"], input[type=\"checkbox\"][name=\"selectall\"]',\n};\n\nexport default class extends BulkActions {\n /** @type {number} The course module ID. */\n #cmid;\n\n /** @type {boolean} Whether to show the extend action. */\n #extend;\n\n /** @type {boolean} Whether to show the grant extension action. */\n #grantAttempt;\n\n /** @type {boolean} Whether to show the set marking allocation action. */\n #markingAllocation;\n\n /** @type {string} Whether to show the message action. */\n #message;\n\n /** @type {Array} The list of plugin operations. */\n #pluginOperations;\n\n /** @type {boolean} Whether to show the remove submission action. */\n #removeSubmission;\n\n /** @type {string} The session key. */\n #sesskey;\n\n /** @type {boolean} Whether to show the revert to draft action. */\n #submissionDrafts;\n\n /** @type {boolean} Whether to show the set workflow state action. */\n #workflowState;\n\n /**\n * Returns the instance of the class.\n *\n * @param {Object} options - The options object.\n * @param {number} options.cmid - The course module ID.\n * @param {string} options.message - Whether to show the message action.\n * @param {boolean} options.submissiondrafts - Whether to show the revert to draft action.\n * @param {boolean} options.removesubmission - Whether to show the remove submission action.\n * @param {boolean} options.extend - Whether to show the grant extension action.\n * @param {boolean} options.grantattempt - Whether to show the grant attempt action.\n * @param {boolean} options.workflowstate - Whether to show the set workflow state action.\n * @param {boolean} options.markingallocation - Whether to show the set marking allocation action.\n * @param {Array} options.pluginoperations - The list of plugin operations.\n * @param {string} options.sesskey - The session key.\n * @returns {this} An instance of the anonymous class extending BulkActions.\n */\n static init(options) {\n return new this(options);\n }\n\n /**\n * The class constructor\n *\n * @param {Object} options - The options object.\n * @param {number} options.cmid - The course module ID.\n * @param {string} options.message - Whether to show the message action.\n * @param {boolean} options.submissiondrafts - Whether to show the revert to draft action.\n * @param {boolean} options.removesubmission - Whether to show the remove submission action.\n * @param {boolean} options.extend - Whether to show the grant extension action.\n * @param {boolean} options.grantattempt - Whether to show the grant attempt action.\n * @param {boolean} options.workflowstate - Whether to show the set workflow state action.\n * @param {boolean} options.markingallocation - Whether to show the set marking allocation action.\n * @param {Array} options.pluginoperations - The list of plugin operations.\n * @param {string} options.sesskey - The session key.\n */\n constructor({\n cmid, message, submissiondrafts, removesubmission, extend,\n grantattempt, workflowstate, markingallocation, pluginoperations, sesskey\n }) {\n super();\n this.#cmid = cmid;\n this.#message = message;\n this.#submissionDrafts = submissiondrafts;\n this.#removeSubmission = removesubmission;\n this.#extend = extend;\n this.#grantAttempt = grantattempt;\n this.#workflowState = workflowstate;\n this.#markingAllocation = markingallocation;\n this.#sesskey = sesskey;\n this.#pluginOperations = pluginoperations;\n }\n\n getBulkActions() {\n const actions = [\n new GeneralAction(\n this.#cmid,\n this.#sesskey,\n 'lock',\n getString('batchoperationlock', 'mod_assign'),\n Templates.renderPix('i/lock', 'core'),\n getString('locksubmissions', 'mod_assign'),\n getString('batchoperationconfirmlock', 'mod_assign'),\n getString('batchoperationlock', 'mod_assign'),\n ),\n new GeneralAction(\n this.#cmid,\n this.#sesskey,\n 'unlock',\n getString('batchoperationunlock', 'mod_assign'),\n Templates.renderPix('i/unlock', 'core'),\n getString('unlocksubmissions', 'mod_assign'),\n getString('batchoperationconfirmunlock', 'mod_assign'),\n getString('batchoperationunlock', 'mod_assign'),\n ),\n new GeneralAction(\n this.#cmid,\n this.#sesskey,\n 'downloadselected',\n getString('batchoperationdownloadselected', 'mod_assign'),\n Templates.renderPix('t/download', 'core'),\n getString('downloadselectedsubmissions', 'mod_assign'),\n getString('batchoperationconfirmdownloadselected', 'mod_assign'),\n getString('batchoperationdownloadselected', 'mod_assign'),\n ),\n ];\n\n if (this.#removeSubmission) {\n actions.push(new DeleteAction(this.#cmid, this.#sesskey));\n }\n\n if (this.#extend) {\n actions.push(new ExtendAction(this.#cmid, this.#sesskey));\n }\n\n if (this.#grantAttempt) {\n actions.push(\n new GeneralAction(\n this.#cmid,\n this.#sesskey,\n 'addattempt',\n getString('batchoperationaddattempt', 'mod_assign'),\n Templates.renderPix('t/add', 'core'),\n getString('addattempt', 'mod_assign'),\n getString('batchoperationconfirmaddattempt', 'mod_assign'),\n getString('batchoperationaddattempt', 'mod_assign'),\n )\n );\n }\n\n if (this.#workflowState) {\n actions.push(new SetMarkingWorkflowStateAction(this.#cmid, this.#sesskey));\n }\n\n if (this.#markingAllocation) {\n actions.push(new SetMarkingAllocationAction(this.#cmid, this.#sesskey));\n }\n\n if (this.#submissionDrafts) {\n actions.push(\n new GeneralAction(\n this.#cmid,\n this.#sesskey,\n 'reverttodraft',\n getString('batchoperationreverttodraft', 'mod_assign'),\n Templates.renderPix('e/undo', 'core'),\n getString('reverttodraft', 'mod_assign'),\n getString('batchoperationconfirmreverttodraft', 'mod_assign'),\n getString('batchoperationreverttodraft', 'mod_assign'),\n )\n );\n }\n\n if (this.#message) {\n actions.push(new MessageAction());\n }\n\n for (const operation of this.#pluginOperations) {\n actions.push(\n new GeneralAction(\n this.#cmid,\n this.#sesskey,\n operation.key,\n operation.label,\n operation.icon,\n operation.confirmationtitle,\n operation.confirmationquestion,\n )\n );\n }\n\n return actions;\n }\n\n getSelectedItems() {\n return document.querySelectorAll(`${Selectors.selectBulkItemCheckbox}:checked`);\n }\n\n registerItemSelectChangeEvent(eventHandler) {\n const itemSelectCheckboxes = document.querySelectorAll(Selectors.selectBulkItemTrigger);\n itemSelectCheckboxes.forEach((checkbox) => {\n checkbox.addEventListener('change', eventHandler.bind(this));\n });\n }\n\n deselectItem(selectedItem) {\n selectedItem.checked = false;\n selectedItem.closest('tr').classList.replace('selectedrow', 'unselectedrow');\n }\n}\n"],"names":["Selectors","BulkActions","options","this","constructor","cmid","message","submissiondrafts","removesubmission","extend","grantattempt","workflowstate","markingallocation","pluginoperations","sesskey","getBulkActions","actions","GeneralAction","Templates","renderPix","push","DeleteAction","ExtendAction","SetMarkingWorkflowStateAction","SetMarkingAllocationAction","MessageAction","operation","key","label","icon","confirmationtitle","confirmationquestion","getSelectedItems","document","querySelectorAll","registerItemSelectChangeEvent","eventHandler","forEach","checkbox","addEventListener","bind","deselectItem","selectedItem","checked","closest","classList","replace"],"mappings":"i9DAiCMA,iCACsB,+CADtBA,gCAEqB,kXAGEC,kCA+CbC,gBACD,IAAIC,KAAKD,SAkBpBE,sBAAYC,KACRA,KADQC,QACFA,QADEC,iBACOA,iBADPC,iBACyBA,iBADzBC,OAC2CA,OAD3CC,aAERA,aAFQC,cAEMA,cAFNC,kBAEqBA,kBAFrBC,iBAEwCA,iBAFxCC,QAE0DA,qxBAGrDT,0CACGC,sDACSC,+DACAC,qDACVC,iDACMC,wDACCC,6DACIC,uDACVE,sDACSD,kBAG7BE,uBACUC,QAAU,CACZ,IAAIC,8CACAd,kCACAA,eACA,QACA,kBAAU,qBAAsB,cAChCe,mBAAUC,UAAU,SAAU,SAC9B,kBAAU,kBAAmB,eAC7B,kBAAU,4BAA6B,eACvC,kBAAU,qBAAsB,eAEpC,IAAIF,8CACAd,kCACAA,eACA,UACA,kBAAU,uBAAwB,cAClCe,mBAAUC,UAAU,WAAY,SAChC,kBAAU,oBAAqB,eAC/B,kBAAU,8BAA+B,eACzC,kBAAU,uBAAwB,eAEtC,IAAIF,8CACAd,kCACAA,eACA,oBACA,kBAAU,iCAAkC,cAC5Ce,mBAAUC,UAAU,aAAc,SAClC,kBAAU,8BAA+B,eACzC,kBAAU,wCAAyC,eACnD,kBAAU,iCAAkC,sCAIhDhB,yBACAa,QAAQI,KAAK,IAAIC,sCAAalB,kCAAYA,uCAG1CA,eACAa,QAAQI,KAAK,IAAIE,uCAAanB,kCAAYA,uCAG1CA,qBACAa,QAAQI,KACJ,IAAIH,8CACAd,kCACAA,eACA,cACA,kBAAU,2BAA4B,cACtCe,mBAAUC,UAAU,QAAS,SAC7B,kBAAU,aAAc,eACxB,kBAAU,kCAAmC,eAC7C,kBAAU,2BAA4B,sCAK9ChB,sBACAa,QAAQI,KAAK,IAAIG,uDAA8BpB,kCAAYA,uCAG3DA,0BACAa,QAAQI,KAAK,IAAII,oDAA2BrB,kCAAYA,uCAGxDA,yBACAa,QAAQI,KACJ,IAAIH,8CACAd,kCACAA,eACA,iBACA,kBAAU,8BAA+B,cACzCe,mBAAUC,UAAU,SAAU,SAC9B,kBAAU,gBAAiB,eAC3B,kBAAU,qCAAsC,eAChD,kBAAU,8BAA+B,sCAKjDhB,gBACAa,QAAQI,KAAK,IAAIK,uBAGhB,MAAMC,mCAAavB,wBACpBa,QAAQI,KACJ,IAAIH,8CACAd,kCACAA,eACAuB,UAAUC,IACVD,UAAUE,MACVF,UAAUG,KACVH,UAAUI,kBACVJ,UAAUK,8BAKff,QAGXgB,0BACWC,SAASC,2BAAoBlC,8CAGxCmC,8BAA8BC,cACGH,SAASC,iBAAiBlC,iCAClCqC,SAASC,WAC1BA,SAASC,iBAAiB,SAAUH,aAAaI,KAAKrC,UAI9DsC,aAAaC,cACTA,aAAaC,SAAU,EACvBD,aAAaE,QAAQ,MAAMC,UAAUC,QAAQ,cAAe"}PK\i extend.min.jsnu[define("mod_assign/bulkactions/grading/extend",["exports","core/bulkactions/bulk_action","core/templates","core/str","core/modal_save_cancel","core/modal_events"],(function(_exports,_bulk_action,_templates,_str,_modal_save_cancel,_modal_events){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classPrivateFieldInitSpec(obj,privateMap,value){!function(obj,privateCollection){if(privateCollection.has(obj))throw new TypeError("Cannot initialize the same private elements twice on an object")}(obj,privateMap),privateMap.set(obj,value)}function _classPrivateFieldGet(receiver,privateMap){return function(receiver,descriptor){if(descriptor.get)return descriptor.get.call(receiver);return descriptor.value}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"get"))}function _classPrivateFieldSet(receiver,privateMap,value){return function(receiver,descriptor,value){if(descriptor.set)descriptor.set.call(receiver,value);else{if(!descriptor.writable)throw new TypeError("attempted to set read only private field");descriptor.value=value}}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"set"),value),value}function _classExtractFieldDescriptor(receiver,privateMap,action){if(!privateMap.has(receiver))throw new TypeError("attempted to "+action+" private field on non-instance");return privateMap.get(receiver)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_bulk_action=_interopRequireDefault(_bulk_action),_templates=_interopRequireDefault(_templates),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_modal_events=_interopRequireDefault(_modal_events);const Selectors_selectBulkItemCheckbox='input[type="checkbox"][name="selectedusers"]:checked';var _cmid=new WeakMap,_sesskey=new WeakMap;class _default extends _bulk_action.default{constructor(cmid,sesskey){super(),_classPrivateFieldInitSpec(this,_cmid,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_sesskey,{writable:!0,value:void 0}),_classPrivateFieldSet(this,_cmid,cmid),_classPrivateFieldSet(this,_sesskey,sesskey)}getBulkActionTriggerSelector(){return'[data-type="bulkactions"] [data-action="grantextension"]'}async triggerBulkAction(){const selectedUsers=[...document.querySelectorAll(Selectors_selectBulkItemCheckbox)].map((checkbox=>checkbox.value)),modal=await _modal_save_cancel.default.create({title:await(0,_str.getString)("grantextension","mod_assign"),buttons:{save:await(0,_str.getString)("batchoperationgrantextension","mod_assign")},body:_templates.default.render("mod_assign/bulkactions/grading/bulk_action_modal_body",{text:await(0,_str.getString)("batchoperationconfirmgrantextension","mod_assign"),operation:"grantextension",cmid:_classPrivateFieldGet(this,_cmid),selectedusers:selectedUsers.join(","),sesskey:_classPrivateFieldGet(this,_sesskey)}),show:!0,removeOnClose:!0});modal.getRoot().on(_modal_events.default.save,(e=>{e.preventDefault(),modal.getRoot().find("form").submit()}))}async renderBulkActionTrigger(showInDropdown,index){return _templates.default.render("mod_assign/bulkactions/grading/bulk_extend_trigger",{showindropdown:showInDropdown,isfirst:0===index})}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=extend.min.js.mapPK\\]message.min.jsnu[define("mod_assign/bulkactions/grading/message",["exports","core/bulkactions/bulk_action","core/templates","core_message/message_send_bulk"],(function(_exports,_bulk_action,_templates,_message_send_bulk){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Bulk action for messaging users in the assignment grading page. * * @module mod_assign/bulkactions/grading/message * @copyright 2024 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_bulk_action=_interopRequireDefault(_bulk_action),_templates=_interopRequireDefault(_templates);const Selectors_selectBulkItemCheckbox='input[type="checkbox"][name="selectedusers"]:checked';class _default extends _bulk_action.default{getBulkActionTriggerSelector(){return'[data-type="bulkactions"] [data-action="message"]'}triggerBulkAction(){const selectedUsers=[...document.querySelectorAll(Selectors_selectBulkItemCheckbox)].map((checkbox=>checkbox.value));(0,_message_send_bulk.showModal)(selectedUsers)}async renderBulkActionTrigger(showInDropdown,index){return _templates.default.render("mod_assign/bulkactions/grading/bulk_message_trigger",{showindropdown:showInDropdown,isfirst:0===index})}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=message.min.js.mapPK\?Mgeneral_action.min.js.mapnu[{"version":3,"file":"general_action.min.js","sources":["../../../src/bulkactions/grading/general_action.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Class that defines the bulk action for general actions in the assignment grading page.\n *\n * @module mod_assign/bulkactions/grading/general_action\n * @copyright 2024 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BulkAction from 'core/bulkactions/bulk_action';\nimport Templates from 'core/templates';\nimport SaveCancelModal from 'core/modal_save_cancel';\nimport ModalEvents from 'core/modal_events';\n\nconst Selectors = {\n selectBulkItemCheckbox: 'input[type=\"checkbox\"][name=\"selectedusers\"]:checked',\n};\n\nexport default class extends BulkAction {\n\n /** @type {string} The action key. */\n actionKey;\n\n /** @type {number} The course module ID. */\n #cmid;\n\n /** @type {Promise} The action button's icon. */\n #buttonIcon;\n\n /** @type {Promise} The action button's label. */\n #buttonLabel;\n\n /** @type {Promise} Title of the confirmation dialog. */\n #confirmationTitle;\n\n /** @type {Promise} Question of the confirmation dialog. */\n #confirmationQuestion;\n\n /** @type {Promise} Text for the confirmation yes button. */\n #confirmationYes;\n\n /** @type {string} The session key. */\n #sesskey;\n\n /**\n * The class constructor.\n *\n * @param {int} cmid The course module ID.\n * @param {string} sesskey The session key.\n * @param {string} actionKey The action key.\n * @param {Promise} buttonLabel The action button's label.\n * @param {Promise} buttonIcon The action button's icon.\n * @param {Promise} confirmationTitle Title of the confirmation dialog.\n * @param {Promise} confirmationQuestion Question of the confirmation dialog.\n * @param {Promise} confirmationYes Text for the confirmation yes button.\n */\n constructor(cmid, sesskey, actionKey, buttonLabel, buttonIcon, confirmationTitle, confirmationQuestion, confirmationYes) {\n super();\n this.#cmid = cmid;\n this.#sesskey = sesskey;\n this.actionKey = actionKey;\n this.#buttonLabel = buttonLabel;\n this.#buttonIcon = buttonIcon;\n this.#confirmationTitle = confirmationTitle;\n this.#confirmationQuestion = confirmationQuestion;\n this.#confirmationYes = confirmationYes;\n }\n\n getBulkActionTriggerSelector() {\n return `[data-type=\"bulkactions\"] [data-action=\"${this.actionKey}\"]`;\n }\n\n async triggerBulkAction() {\n const selectedUsers = [...document.querySelectorAll(Selectors.selectBulkItemCheckbox)].map(checkbox => checkbox.value);\n\n const modal = await SaveCancelModal.create({\n title: await this.#confirmationTitle,\n buttons: {\n save: await this.#confirmationYes,\n },\n body: Templates.render('mod_assign/bulkactions/grading/bulk_action_modal_body', {\n text: await this.#confirmationQuestion,\n operation: this.actionKey,\n cmid: this.#cmid,\n selectedusers: selectedUsers.join(','),\n sesskey: this.#sesskey\n }),\n show: true,\n removeOnClose: true,\n });\n\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, () => {\n modal.getRoot().find('form').submit();\n });\n }\n\n async renderBulkActionTrigger(showInDropdown, index) {\n return Templates.render('mod_assign/bulkactions/grading/bulk_general_action_trigger', {\n action: this.actionKey,\n title: await this.#buttonLabel,\n icon: await this.#buttonIcon,\n showindropdown: showInDropdown,\n isfirst: index === 0,\n });\n }\n}\n"],"names":["Selectors","BulkAction","constructor","cmid","sesskey","actionKey","buttonLabel","buttonIcon","confirmationTitle","confirmationQuestion","confirmationYes","getBulkActionTriggerSelector","this","selectedUsers","document","querySelectorAll","map","checkbox","value","modal","SaveCancelModal","create","title","buttons","save","body","Templates","render","text","operation","selectedusers","join","show","removeOnClose","getRoot","on","ModalEvents","find","submit","showInDropdown","index","action","icon","showindropdown","isfirst"],"mappings":"unDA4BMA,iCACsB,wQAGCC,qBAsCzBC,YAAYC,KAAMC,QAASC,UAAWC,YAAaC,WAAYC,kBAAmBC,qBAAsBC,muBAEvFP,0CACGC,cACXC,UAAYA,kDACGC,oDACDC,0DACOC,oEACGC,kEACLC,iBAG5BC,uFACsDC,KAAKP,gDAIjDQ,cAAgB,IAAIC,SAASC,iBAAiBf,mCAAmCgB,KAAIC,UAAYA,SAASC,QAE1GC,YAAcC,2BAAgBC,OAAO,CACvCC,kCAAaV,yBACbW,QAAS,CACLC,iCAAYZ,wBAEhBa,KAAMC,mBAAUC,OAAO,wDAAyD,CAC5EC,iCAAYhB,4BACZiB,UAAWjB,KAAKP,UAChBF,2BAAMS,YACNkB,cAAejB,cAAckB,KAAK,KAClC3B,8BAASQ,iBAEboB,MAAM,EACNC,eAAe,IAInBd,MAAMe,UAAUC,GAAGC,sBAAYZ,MAAM,KACjCL,MAAMe,UAAUG,KAAK,QAAQC,0CAIPC,eAAgBC,cACnCd,mBAAUC,OAAO,6DAA8D,CAClFc,OAAQ7B,KAAKP,UACbiB,kCAAaV,mBACb8B,iCAAY9B,kBACZ+B,eAAgBJ,eAChBK,QAAmB,IAAVJ"}PK\L=hh"setmarkingworkflowstate.min.js.mapnu[{"version":3,"file":"setmarkingworkflowstate.min.js","sources":["../../../src/bulkactions/grading/setmarkingworkflowstate.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Class that defines the bulk action for setting marking workflow state in the assignment grading page.\n *\n * @module mod_assign/bulkactions/grading/setmarkingworkflowstate\n * @copyright 2024 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BulkAction from 'core/bulkactions/bulk_action';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport SaveCancelModal from 'core/modal_save_cancel';\nimport ModalEvents from 'core/modal_events';\n\nconst Selectors = {\n selectBulkItemCheckbox: 'input[type=\"checkbox\"][name=\"selectedusers\"]:checked',\n};\n\nexport default class extends BulkAction {\n /** @type {number} The course module ID. */\n #cmid;\n\n /** @type {string} The session key. */\n #sesskey;\n\n /**\n * The class constructor.\n *\n * @param {number} cmid The course module ID.\n * @param {string} sesskey The session key.\n */\n constructor(cmid, sesskey) {\n super();\n this.#cmid = cmid;\n this.#sesskey = sesskey;\n }\n\n getBulkActionTriggerSelector() {\n return '[data-type=\"bulkactions\"] [data-action=\"setmarkingworkflowstate\"]';\n }\n\n async triggerBulkAction() {\n const selectedUsers = [...document.querySelectorAll(Selectors.selectBulkItemCheckbox)].map(checkbox => checkbox.value);\n\n const modal = await SaveCancelModal.create({\n title: await getString('setmarkingworkflowstate', 'mod_assign'),\n buttons: {\n save: await getString('batchoperationsetmarkingworkflowstate', 'mod_assign'),\n },\n body: Templates.render('mod_assign/bulkactions/grading/bulk_action_modal_body', {\n text: await getString('batchoperationconfirmsetmarkingworkflowstate', 'mod_assign'),\n operation: 'setmarkingworkflowstate',\n cmid: this.#cmid,\n selectedusers: selectedUsers.join(','),\n sesskey: this.#sesskey\n }),\n show: true,\n removeOnClose: true,\n });\n\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, (e) => {\n e.preventDefault();\n modal.getRoot().find('form').submit();\n });\n }\n\n async renderBulkActionTrigger(showInDropdown, index) {\n return Templates.render('mod_assign/bulkactions/grading/bulk_setmarkingworkflowstate_trigger', {\n showindropdown: showInDropdown,\n isfirst: index === 0,\n });\n }\n}\n"],"names":["Selectors","BulkAction","constructor","cmid","sesskey","getBulkActionTriggerSelector","selectedUsers","document","querySelectorAll","map","checkbox","value","modal","SaveCancelModal","create","title","buttons","save","body","Templates","render","text","operation","this","selectedusers","join","show","removeOnClose","getRoot","on","ModalEvents","e","preventDefault","find","submit","showInDropdown","index","showindropdown","isfirst"],"mappings":"gpDA6BMA,iCACsB,yHAGCC,qBAazBC,YAAYC,KAAMC,yLAEDD,0CACGC,SAGpBC,qCACW,oGAIDC,cAAgB,IAAIC,SAASC,iBAAiBR,mCAAmCS,KAAIC,UAAYA,SAASC,QAE1GC,YAAcC,2BAAgBC,OAAO,CACvCC,YAAa,kBAAU,0BAA2B,cAClDC,QAAS,CACLC,WAAY,kBAAU,wCAAyC,eAEnEC,KAAMC,mBAAUC,OAAO,wDAAyD,CAC5EC,WAAY,kBAAU,+CAAgD,cACtEC,UAAW,0BACXnB,2BAAMoB,YACNC,cAAelB,cAAcmB,KAAK,KAClCrB,8BAASmB,iBAEbG,MAAM,EACNC,eAAe,IAInBf,MAAMgB,UAAUC,GAAGC,sBAAYb,MAAOc,IAClCA,EAAEC,iBACFpB,MAAMgB,UAAUK,KAAK,QAAQC,0CAIPC,eAAgBC,cACnCjB,mBAAUC,OAAO,sEAAuE,CAC3FiB,eAAgBF,eAChBG,QAAmB,IAAVF"}PK\?iextend.min.js.mapnu[{"version":3,"file":"extend.min.js","sources":["../../../src/bulkactions/grading/extend.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Class that defines the bulk action to extend the assignment deadline in the assignment grading page.\n *\n * @module mod_assign/bulkactions/grading/extend\n * @copyright 2024 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BulkAction from 'core/bulkactions/bulk_action';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport SaveCancelModal from 'core/modal_save_cancel';\nimport ModalEvents from 'core/modal_events';\n\nconst Selectors = {\n selectBulkItemCheckbox: 'input[type=\"checkbox\"][name=\"selectedusers\"]:checked',\n};\n\nexport default class extends BulkAction {\n /** @type {number} The course module ID. */\n #cmid;\n\n /** @type {string} The session key. */\n #sesskey;\n\n /**\n * The class constructor.\n *\n * @param {number} cmid The course module ID.\n * @param {string} sesskey The session key.\n */\n constructor(cmid, sesskey) {\n super();\n this.#cmid = cmid;\n this.#sesskey = sesskey;\n }\n\n getBulkActionTriggerSelector() {\n return '[data-type=\"bulkactions\"] [data-action=\"grantextension\"]';\n }\n\n async triggerBulkAction() {\n const selectedUsers = [...document.querySelectorAll(Selectors.selectBulkItemCheckbox)].map(checkbox => checkbox.value);\n\n const modal = await SaveCancelModal.create({\n title: await getString('grantextension', 'mod_assign'),\n buttons: {\n save: await getString('batchoperationgrantextension', 'mod_assign'),\n },\n body: Templates.render('mod_assign/bulkactions/grading/bulk_action_modal_body', {\n text: await getString('batchoperationconfirmgrantextension', 'mod_assign'),\n operation: 'grantextension',\n cmid: this.#cmid,\n selectedusers: selectedUsers.join(','),\n sesskey: this.#sesskey\n }),\n show: true,\n removeOnClose: true,\n });\n\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, (e) => {\n e.preventDefault();\n modal.getRoot().find('form').submit();\n });\n }\n\n async renderBulkActionTrigger(showInDropdown, index) {\n return Templates.render('mod_assign/bulkactions/grading/bulk_extend_trigger', {\n showindropdown: showInDropdown,\n isfirst: index === 0,\n });\n }\n}\n"],"names":["Selectors","BulkAction","constructor","cmid","sesskey","getBulkActionTriggerSelector","selectedUsers","document","querySelectorAll","map","checkbox","value","modal","SaveCancelModal","create","title","buttons","save","body","Templates","render","text","operation","this","selectedusers","join","show","removeOnClose","getRoot","on","ModalEvents","e","preventDefault","find","submit","showInDropdown","index","showindropdown","isfirst"],"mappings":"+nDA6BMA,iCACsB,yHAGCC,qBAazBC,YAAYC,KAAMC,yLAEDD,0CACGC,SAGpBC,qCACW,2FAIDC,cAAgB,IAAIC,SAASC,iBAAiBR,mCAAmCS,KAAIC,UAAYA,SAASC,QAE1GC,YAAcC,2BAAgBC,OAAO,CACvCC,YAAa,kBAAU,iBAAkB,cACzCC,QAAS,CACLC,WAAY,kBAAU,+BAAgC,eAE1DC,KAAMC,mBAAUC,OAAO,wDAAyD,CAC5EC,WAAY,kBAAU,sCAAuC,cAC7DC,UAAW,iBACXnB,2BAAMoB,YACNC,cAAelB,cAAcmB,KAAK,KAClCrB,8BAASmB,iBAEbG,MAAM,EACNC,eAAe,IAInBf,MAAMgB,UAAUC,GAAGC,sBAAYb,MAAOc,IAClCA,EAAEC,iBACFpB,MAAMgB,UAAUK,KAAK,QAAQC,0CAIPC,eAAgBC,cACnCjB,mBAAUC,OAAO,qDAAsD,CAC1EiB,eAAgBF,eAChBG,QAAmB,IAAVF"}PK\C5ddbulk_actions.min.jsnu[define("mod_assign/bulkactions/grading/bulk_actions",["exports","core/bulkactions/bulk_actions","./general_action","./delete","./extend","./message","./setmarkingallocation","./setmarkingworkflowstate","core/templates","core/str"],(function(_exports,_bulk_actions,_general_action,_delete,_extend2,_message2,_setmarkingallocation,_setmarkingworkflowstate,_templates,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classPrivateFieldInitSpec(obj,privateMap,value){!function(obj,privateCollection){if(privateCollection.has(obj))throw new TypeError("Cannot initialize the same private elements twice on an object")}(obj,privateMap),privateMap.set(obj,value)}function _classPrivateFieldGet(receiver,privateMap){return function(receiver,descriptor){if(descriptor.get)return descriptor.get.call(receiver);return descriptor.value}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"get"))}function _classPrivateFieldSet(receiver,privateMap,value){return function(receiver,descriptor,value){if(descriptor.set)descriptor.set.call(receiver,value);else{if(!descriptor.writable)throw new TypeError("attempted to set read only private field");descriptor.value=value}}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"set"),value),value}function _classExtractFieldDescriptor(receiver,privateMap,action){if(!privateMap.has(receiver))throw new TypeError("attempted to "+action+" private field on non-instance");return privateMap.get(receiver)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_bulk_actions=_interopRequireDefault(_bulk_actions),_general_action=_interopRequireDefault(_general_action),_delete=_interopRequireDefault(_delete),_extend2=_interopRequireDefault(_extend2),_message2=_interopRequireDefault(_message2),_setmarkingallocation=_interopRequireDefault(_setmarkingallocation),_setmarkingworkflowstate=_interopRequireDefault(_setmarkingworkflowstate),_templates=_interopRequireDefault(_templates);const Selectors_selectBulkItemCheckbox='input[type="checkbox"][name="selectedusers"]',Selectors_selectBulkItemTrigger='input[type="checkbox"][name="selectedusers"], input[type="checkbox"][name="selectall"]';var _cmid=new WeakMap,_extend=new WeakMap,_grantAttempt=new WeakMap,_markingAllocation=new WeakMap,_message=new WeakMap,_pluginOperations=new WeakMap,_removeSubmission=new WeakMap,_sesskey=new WeakMap,_submissionDrafts=new WeakMap,_workflowState=new WeakMap;class _default extends _bulk_actions.default{static init(options){return new this(options)}constructor(_ref){let{cmid:cmid,message:message,submissiondrafts:submissiondrafts,removesubmission:removesubmission,extend:extend,grantattempt:grantattempt,workflowstate:workflowstate,markingallocation:markingallocation,pluginoperations:pluginoperations,sesskey:sesskey}=_ref;super(),_classPrivateFieldInitSpec(this,_cmid,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_extend,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_grantAttempt,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_markingAllocation,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_message,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_pluginOperations,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_removeSubmission,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_sesskey,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_submissionDrafts,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_workflowState,{writable:!0,value:void 0}),_classPrivateFieldSet(this,_cmid,cmid),_classPrivateFieldSet(this,_message,message),_classPrivateFieldSet(this,_submissionDrafts,submissiondrafts),_classPrivateFieldSet(this,_removeSubmission,removesubmission),_classPrivateFieldSet(this,_extend,extend),_classPrivateFieldSet(this,_grantAttempt,grantattempt),_classPrivateFieldSet(this,_workflowState,workflowstate),_classPrivateFieldSet(this,_markingAllocation,markingallocation),_classPrivateFieldSet(this,_sesskey,sesskey),_classPrivateFieldSet(this,_pluginOperations,pluginoperations)}getBulkActions(){const actions=[new _general_action.default(_classPrivateFieldGet(this,_cmid),_classPrivateFieldGet(this,_sesskey),"lock",(0,_str.getString)("batchoperationlock","mod_assign"),_templates.default.renderPix("i/lock","core"),(0,_str.getString)("locksubmissions","mod_assign"),(0,_str.getString)("batchoperationconfirmlock","mod_assign"),(0,_str.getString)("batchoperationlock","mod_assign")),new _general_action.default(_classPrivateFieldGet(this,_cmid),_classPrivateFieldGet(this,_sesskey),"unlock",(0,_str.getString)("batchoperationunlock","mod_assign"),_templates.default.renderPix("i/unlock","core"),(0,_str.getString)("unlocksubmissions","mod_assign"),(0,_str.getString)("batchoperationconfirmunlock","mod_assign"),(0,_str.getString)("batchoperationunlock","mod_assign")),new _general_action.default(_classPrivateFieldGet(this,_cmid),_classPrivateFieldGet(this,_sesskey),"downloadselected",(0,_str.getString)("batchoperationdownloadselected","mod_assign"),_templates.default.renderPix("t/download","core"),(0,_str.getString)("downloadselectedsubmissions","mod_assign"),(0,_str.getString)("batchoperationconfirmdownloadselected","mod_assign"),(0,_str.getString)("batchoperationdownloadselected","mod_assign"))];_classPrivateFieldGet(this,_removeSubmission)&&actions.push(new _delete.default(_classPrivateFieldGet(this,_cmid),_classPrivateFieldGet(this,_sesskey))),_classPrivateFieldGet(this,_extend)&&actions.push(new _extend2.default(_classPrivateFieldGet(this,_cmid),_classPrivateFieldGet(this,_sesskey))),_classPrivateFieldGet(this,_grantAttempt)&&actions.push(new _general_action.default(_classPrivateFieldGet(this,_cmid),_classPrivateFieldGet(this,_sesskey),"addattempt",(0,_str.getString)("batchoperationaddattempt","mod_assign"),_templates.default.renderPix("t/add","core"),(0,_str.getString)("addattempt","mod_assign"),(0,_str.getString)("batchoperationconfirmaddattempt","mod_assign"),(0,_str.getString)("batchoperationaddattempt","mod_assign"))),_classPrivateFieldGet(this,_workflowState)&&actions.push(new _setmarkingworkflowstate.default(_classPrivateFieldGet(this,_cmid),_classPrivateFieldGet(this,_sesskey))),_classPrivateFieldGet(this,_markingAllocation)&&actions.push(new _setmarkingallocation.default(_classPrivateFieldGet(this,_cmid),_classPrivateFieldGet(this,_sesskey))),_classPrivateFieldGet(this,_submissionDrafts)&&actions.push(new _general_action.default(_classPrivateFieldGet(this,_cmid),_classPrivateFieldGet(this,_sesskey),"reverttodraft",(0,_str.getString)("batchoperationreverttodraft","mod_assign"),_templates.default.renderPix("e/undo","core"),(0,_str.getString)("reverttodraft","mod_assign"),(0,_str.getString)("batchoperationconfirmreverttodraft","mod_assign"),(0,_str.getString)("batchoperationreverttodraft","mod_assign"))),_classPrivateFieldGet(this,_message)&&actions.push(new _message2.default);for(const operation of _classPrivateFieldGet(this,_pluginOperations))actions.push(new _general_action.default(_classPrivateFieldGet(this,_cmid),_classPrivateFieldGet(this,_sesskey),operation.key,operation.label,operation.icon,operation.confirmationtitle,operation.confirmationquestion));return actions}getSelectedItems(){return document.querySelectorAll("".concat(Selectors_selectBulkItemCheckbox,":checked"))}registerItemSelectChangeEvent(eventHandler){document.querySelectorAll(Selectors_selectBulkItemTrigger).forEach((checkbox=>{checkbox.addEventListener("change",eventHandler.bind(this))}))}deselectItem(selectedItem){selectedItem.checked=!1,selectedItem.closest("tr").classList.replace("selectedrow","unselectedrow")}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=bulk_actions.min.js.mapPK\W delete.min.jsnu[define("mod_assign/bulkactions/grading/delete",["exports","core/bulkactions/bulk_action","core/templates","core/str","core/modal_delete_cancel","core/modal_events"],(function(_exports,_bulk_action,_templates,_str,_modal_delete_cancel,_modal_events){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classPrivateFieldInitSpec(obj,privateMap,value){!function(obj,privateCollection){if(privateCollection.has(obj))throw new TypeError("Cannot initialize the same private elements twice on an object")}(obj,privateMap),privateMap.set(obj,value)}function _classPrivateFieldGet(receiver,privateMap){return function(receiver,descriptor){if(descriptor.get)return descriptor.get.call(receiver);return descriptor.value}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"get"))}function _classPrivateFieldSet(receiver,privateMap,value){return function(receiver,descriptor,value){if(descriptor.set)descriptor.set.call(receiver,value);else{if(!descriptor.writable)throw new TypeError("attempted to set read only private field");descriptor.value=value}}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"set"),value),value}function _classExtractFieldDescriptor(receiver,privateMap,action){if(!privateMap.has(receiver))throw new TypeError("attempted to "+action+" private field on non-instance");return privateMap.get(receiver)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_bulk_action=_interopRequireDefault(_bulk_action),_templates=_interopRequireDefault(_templates),_modal_delete_cancel=_interopRequireDefault(_modal_delete_cancel),_modal_events=_interopRequireDefault(_modal_events);const Selectors_selectBulkItemCheckbox='input[type="checkbox"][name="selectedusers"]:checked';var _cmid=new WeakMap,_sesskey=new WeakMap;class _default extends _bulk_action.default{constructor(cmid,sesskey){super(),_classPrivateFieldInitSpec(this,_cmid,{writable:!0,value:void 0}),_classPrivateFieldInitSpec(this,_sesskey,{writable:!0,value:void 0}),_classPrivateFieldSet(this,_cmid,cmid),_classPrivateFieldSet(this,_sesskey,sesskey)}getBulkActionTriggerSelector(){return'[data-type="bulkactions"] [data-action="removesubmission"]'}async triggerBulkAction(){const selectedUsers=[...document.querySelectorAll(Selectors_selectBulkItemCheckbox)].map((checkbox=>checkbox.value)),modal=await _modal_delete_cancel.default.create({title:await(0,_str.getString)("removesubmission","mod_assign"),buttons:{save:await(0,_str.getString)("batchoperationremovesubmission","mod_assign")},body:_templates.default.render("mod_assign/bulkactions/grading/bulk_action_modal_body",{text:await(0,_str.getString)("batchoperationconfirmremovesubmission","mod_assign"),operation:"removesubmission",cmid:_classPrivateFieldGet(this,_cmid),selectedusers:selectedUsers.join(","),sesskey:_classPrivateFieldGet(this,_sesskey)}),show:!0,removeOnClose:!0});modal.getRoot().on(_modal_events.default.delete,(e=>{e.preventDefault(),modal.getRoot().find("form").submit()}))}async renderBulkActionTrigger(showInDropdown,index){return _templates.default.render("mod_assign/bulkactions/grading/bulk_delete_trigger",{showindropdown:showInDropdown,isfirst:0===index})}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=delete.min.js.mapPK\B  delete.min.js.mapnu[{"version":3,"file":"delete.min.js","sources":["../../../src/bulkactions/grading/delete.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Class that defines the bulk action for removing submissions in the assignment grading page.\n *\n * @module mod_assign/bulkactions/grading/delete\n * @copyright 2024 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BulkAction from 'core/bulkactions/bulk_action';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport DeleteCancelModal from 'core/modal_delete_cancel';\nimport ModalEvents from 'core/modal_events';\n\nconst Selectors = {\n selectBulkItemCheckbox: 'input[type=\"checkbox\"][name=\"selectedusers\"]:checked',\n};\n\nexport default class extends BulkAction {\n /** @type {number} The course module ID. */\n #cmid;\n\n /** @type {string} The session key. */\n #sesskey;\n\n /**\n * The class constructor.\n *\n * @param {number} cmid The course module ID.\n * @param {string} sesskey The session key.\n */\n constructor(cmid, sesskey) {\n super();\n this.#cmid = cmid;\n this.#sesskey = sesskey;\n }\n\n getBulkActionTriggerSelector() {\n return '[data-type=\"bulkactions\"] [data-action=\"removesubmission\"]';\n }\n\n async triggerBulkAction() {\n const selectedUsers = [...document.querySelectorAll(Selectors.selectBulkItemCheckbox)].map(checkbox => checkbox.value);\n\n const modal = await DeleteCancelModal.create({\n title: await getString('removesubmission', 'mod_assign'),\n buttons: {\n save: await getString('batchoperationremovesubmission', 'mod_assign'),\n },\n body: Templates.render('mod_assign/bulkactions/grading/bulk_action_modal_body', {\n text: await getString('batchoperationconfirmremovesubmission', 'mod_assign'),\n operation: 'removesubmission',\n cmid: this.#cmid,\n selectedusers: selectedUsers.join(','),\n sesskey: this.#sesskey\n }),\n show: true,\n removeOnClose: true,\n });\n\n // Handle delete event.\n modal.getRoot().on(ModalEvents.delete, (e) => {\n e.preventDefault();\n modal.getRoot().find('form').submit();\n });\n }\n\n async renderBulkActionTrigger(showInDropdown, index) {\n return Templates.render('mod_assign/bulkactions/grading/bulk_delete_trigger', {\n showindropdown: showInDropdown,\n isfirst: index === 0,\n });\n }\n}\n"],"names":["Selectors","BulkAction","constructor","cmid","sesskey","getBulkActionTriggerSelector","selectedUsers","document","querySelectorAll","map","checkbox","value","modal","DeleteCancelModal","create","title","buttons","save","body","Templates","render","text","operation","this","selectedusers","join","show","removeOnClose","getRoot","on","ModalEvents","delete","e","preventDefault","find","submit","showInDropdown","index","showindropdown","isfirst"],"mappings":"uoDA6BMA,iCACsB,yHAGCC,qBAazBC,YAAYC,KAAMC,yLAEDD,0CACGC,SAGpBC,qCACW,6FAIDC,cAAgB,IAAIC,SAASC,iBAAiBR,mCAAmCS,KAAIC,UAAYA,SAASC,QAE1GC,YAAcC,6BAAkBC,OAAO,CACzCC,YAAa,kBAAU,mBAAoB,cAC3CC,QAAS,CACLC,WAAY,kBAAU,iCAAkC,eAE5DC,KAAMC,mBAAUC,OAAO,wDAAyD,CAC5EC,WAAY,kBAAU,wCAAyC,cAC/DC,UAAW,mBACXnB,2BAAMoB,YACNC,cAAelB,cAAcmB,KAAK,KAClCrB,8BAASmB,iBAEbG,MAAM,EACNC,eAAe,IAInBf,MAAMgB,UAAUC,GAAGC,sBAAYC,QAASC,IACpCA,EAAEC,iBACFrB,MAAMgB,UAAUM,KAAK,QAAQC,0CAIPC,eAAgBC,cACnClB,mBAAUC,OAAO,qDAAsD,CAC1EkB,eAAgBF,eAChBG,QAAmB,IAAVF"}PK\!extra_filters_dropdown.min.js.mapnu[{"version":3,"file":"extra_filters_dropdown.min.js","sources":["../../../src/actionbar/grading/extra_filters_dropdown.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {getDropdownDialog} from 'core/local/dropdown/dialog';\nimport {getUserPreference} from 'core_user/repository';\n\n/**\n * Module for the extra filters dropdown on the submissions page.\n *\n * @module mod_assign/actionbar/grading/extra_filters_dropdown\n * @copyright 2024 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/** @constant {Object} The object containing the relevant selectors. */\nconst Selectors = {\n extraFiltersDropdown: '.dropdown.extrafilters',\n extraFiltersClose: 'a[data-action=\"close\"]',\n workflowFilterElement: 'select[name=\"workflowfilter\"]',\n markerFilterElement: 'select[name=\"markingallocationfilter\"]',\n suspendedParticipantsFilterCheckbox: 'input[type=\"checkbox\"][name=\"suspendedparticipantsfilter\"]',\n suspendedParticipantsFilterHidden: 'input[type=\"hidden\"][name=\"suspendedparticipantsfilter\"]'\n};\n\n/**\n * Register event listeners for the extra filters dropdown.\n *\n * @param {DropdownDialog} extraFiltersDropdown The dropdown dialog instance.\n */\nconst registerEventListeners = (extraFiltersDropdown) => {\n // Click event listener to the extra filters dropdown element.\n extraFiltersDropdown.getElement().addEventListener('click', e => {\n // The target is the 'Close' button.\n if (e.target.closest(Selectors.extraFiltersClose)) {\n e.preventDefault();\n extraFiltersDropdown.setVisible(false);\n }\n });\n\n // Change event listener to the extra filters dropdown element.\n extraFiltersDropdown.getElement().addEventListener('change', e => {\n const suspendedParticipantsFilterCheckbox = e.target.closest(Selectors.suspendedParticipantsFilterCheckbox);\n // The target is the 'Suspended participants' filter checkbox.\n if (suspendedParticipantsFilterCheckbox) {\n // The 'Suspended participants' filter uses a hidden input and a checkbox. The hidden input is used to\n // submit '0' as a workaround when the checkbox is unchecked since unchecked checkboxes are not submitted\n // with the form. Therefore, we need to enable or disable the hidden input based on the checkbox state.\n const suspendedParticipantsFilterHidden = suspendedParticipantsFilterCheckbox.parentNode\n .querySelector(Selectors.suspendedParticipantsFilterHidden);\n suspendedParticipantsFilterHidden.disabled = suspendedParticipantsFilterCheckbox.checked;\n }\n });\n\n // Event listener triggered upon hiding of the dropdown.\n extraFiltersDropdown.getElement().addEventListener('hide.bs.dropdown', () => {\n // Restore the filters to their stored preference values once the dropdown is closed.\n restoreAppliedWorkflowFilter(extraFiltersDropdown);\n restoreAppliedMarkerFilter(extraFiltersDropdown);\n restoreAppliedSuspendedParticipantsFilter(extraFiltersDropdown);\n });\n};\n\n/**\n * Restores the currently applied workflow filter to its stored preference value.\n *\n * @param {DropdownDialog} extraFiltersDropdown The dropdown dialog instance.\n */\nconst restoreAppliedWorkflowFilter = async(extraFiltersDropdown) => {\n const workflowFilterSelect = extraFiltersDropdown.getElement().querySelector(Selectors.workflowFilterElement);\n if (workflowFilterSelect) {\n workflowFilterSelect.value = await getUserPreference('assign_workflowfilter');\n }\n};\n\n/**\n * Restores the currently applied marker filter to its stored preference value.\n *\n * @param {DropdownDialog} extraFiltersDropdown The dropdown dialog instance.\n */\nconst restoreAppliedMarkerFilter = async(extraFiltersDropdown) => {\n const markerFilterSelect = extraFiltersDropdown.getElement().querySelector(Selectors.markerFilterElement);\n if (markerFilterSelect) {\n markerFilterSelect.value = await getUserPreference('assign_markerfilter');\n }\n};\n\n/**\n * Restores the currently suspended participants filter to its stored preference value.\n *\n * @param {DropdownDialog} extraFiltersDropdown The dropdown dialog instance.\n */\nconst restoreAppliedSuspendedParticipantsFilter = async(extraFiltersDropdown) => {\n const suspendedParticipantsFilterCheckbox = extraFiltersDropdown.getElement()\n .querySelector(Selectors.suspendedParticipantsFilterCheckbox);\n if (suspendedParticipantsFilterCheckbox) {\n const suspendedParticipantsFilterHidden = suspendedParticipantsFilterCheckbox.parentNode\n .querySelector(Selectors.suspendedParticipantsFilterHidden);\n const showOnlyActiveParticipants = await getUserPreference('grade_report_showonlyactiveenrol');\n suspendedParticipantsFilterCheckbox.checked = !showOnlyActiveParticipants;\n suspendedParticipantsFilterHidden.disabled = !showOnlyActiveParticipants;\n }\n};\n\n/**\n * Initialize module.\n */\nexport const init = () => {\n const extraFiltersDropdown = getDropdownDialog(Selectors.extraFiltersDropdown);\n if (extraFiltersDropdown) {\n registerEventListeners(extraFiltersDropdown);\n }\n};\n"],"names":["Selectors","restoreAppliedWorkflowFilter","async","workflowFilterSelect","extraFiltersDropdown","getElement","querySelector","value","restoreAppliedMarkerFilter","markerFilterSelect","restoreAppliedSuspendedParticipantsFilter","suspendedParticipantsFilterCheckbox","suspendedParticipantsFilterHidden","parentNode","showOnlyActiveParticipants","checked","disabled","addEventListener","e","target","closest","preventDefault","setVisible","registerEventListeners"],"mappings":";;;;;;;;MA2BMA,+BACoB,yBADpBA,4BAEiB,yBAFjBA,gCAGqB,gCAHrBA,8BAImB,yCAJnBA,8CAKmC,6DALnCA,4CAMiC,2DA8CjCC,6BAA+BC,MAAAA,6BAC3BC,qBAAuBC,qBAAqBC,aAAaC,cAAcN,iCACzEG,uBACAA,qBAAqBI,YAAc,iCAAkB,2BASvDC,2BAA6BN,MAAAA,6BACzBO,mBAAqBL,qBAAqBC,aAAaC,cAAcN,+BACvES,qBACAA,mBAAmBF,YAAc,iCAAkB,yBASrDG,0CAA4CR,MAAAA,6BACxCS,oCAAsCP,qBAAqBC,aAC5DC,cAAcN,kDACfW,oCAAqC,OAC/BC,kCAAoCD,oCAAoCE,WACzEP,cAAcN,6CACbc,iCAAmC,iCAAkB,oCAC3DH,oCAAoCI,SAAWD,2BAC/CF,kCAAkCI,UAAYF,2CAOlC,WACVV,sBAAuB,6BAAkBJ,gCAC3CI,sBA/EwBA,CAAAA,uBAE5BA,qBAAqBC,aAAaY,iBAAiB,SAASC,IAEpDA,EAAEC,OAAOC,QAAQpB,+BACjBkB,EAAEG,iBACFjB,qBAAqBkB,YAAW,OAKxClB,qBAAqBC,aAAaY,iBAAiB,UAAUC,UACnDP,oCAAsCO,EAAEC,OAAOC,QAAQpB,+CAEzDW,sCAI0CA,oCAAoCE,WACzEP,cAAcN,6CACegB,SAAWL,oCAAoCI,YAKzFX,qBAAqBC,aAAaY,iBAAiB,oBAAoB,KAEnEhB,6BAA6BG,sBAC7BI,2BAA2BJ,sBAC3BM,0CAA0CN,0BAmD1CmB,CAAuBnB"}PK\K0n extra_filters_dropdown.min.jsnu[define("mod_assign/actionbar/grading/extra_filters_dropdown",["exports","core/local/dropdown/dialog","core_user/repository"],(function(_exports,_dialog,_repository){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; /** * Module for the extra filters dropdown on the submissions page. * * @module mod_assign/actionbar/grading/extra_filters_dropdown * @copyright 2024 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const Selectors_extraFiltersDropdown=".dropdown.extrafilters",Selectors_extraFiltersClose='a[data-action="close"]',Selectors_workflowFilterElement='select[name="workflowfilter"]',Selectors_markerFilterElement='select[name="markingallocationfilter"]',Selectors_suspendedParticipantsFilterCheckbox='input[type="checkbox"][name="suspendedparticipantsfilter"]',Selectors_suspendedParticipantsFilterHidden='input[type="hidden"][name="suspendedparticipantsfilter"]',restoreAppliedWorkflowFilter=async extraFiltersDropdown=>{const workflowFilterSelect=extraFiltersDropdown.getElement().querySelector(Selectors_workflowFilterElement);workflowFilterSelect&&(workflowFilterSelect.value=await(0,_repository.getUserPreference)("assign_workflowfilter"))},restoreAppliedMarkerFilter=async extraFiltersDropdown=>{const markerFilterSelect=extraFiltersDropdown.getElement().querySelector(Selectors_markerFilterElement);markerFilterSelect&&(markerFilterSelect.value=await(0,_repository.getUserPreference)("assign_markerfilter"))},restoreAppliedSuspendedParticipantsFilter=async extraFiltersDropdown=>{const suspendedParticipantsFilterCheckbox=extraFiltersDropdown.getElement().querySelector(Selectors_suspendedParticipantsFilterCheckbox);if(suspendedParticipantsFilterCheckbox){const suspendedParticipantsFilterHidden=suspendedParticipantsFilterCheckbox.parentNode.querySelector(Selectors_suspendedParticipantsFilterHidden),showOnlyActiveParticipants=await(0,_repository.getUserPreference)("grade_report_showonlyactiveenrol");suspendedParticipantsFilterCheckbox.checked=!showOnlyActiveParticipants,suspendedParticipantsFilterHidden.disabled=!showOnlyActiveParticipants}};_exports.init=()=>{const extraFiltersDropdown=(0,_dialog.getDropdownDialog)(Selectors_extraFiltersDropdown);extraFiltersDropdown&&(extraFiltersDropdown=>{extraFiltersDropdown.getElement().addEventListener("click",(e=>{e.target.closest(Selectors_extraFiltersClose)&&(e.preventDefault(),extraFiltersDropdown.setVisible(!1))})),extraFiltersDropdown.getElement().addEventListener("change",(e=>{const suspendedParticipantsFilterCheckbox=e.target.closest(Selectors_suspendedParticipantsFilterCheckbox);suspendedParticipantsFilterCheckbox&&(suspendedParticipantsFilterCheckbox.parentNode.querySelector(Selectors_suspendedParticipantsFilterHidden).disabled=suspendedParticipantsFilterCheckbox.checked)})),extraFiltersDropdown.getElement().addEventListener("hide.bs.dropdown",(()=>{restoreAppliedWorkflowFilter(extraFiltersDropdown),restoreAppliedMarkerFilter(extraFiltersDropdown),restoreAppliedSuspendedParticipantsFilter(extraFiltersDropdown)}))})(extraFiltersDropdown)}})); //# sourceMappingURL=extra_filters_dropdown.min.js.mapPK'\l8l renderer.phpnu[PK'\) #form/lib.phpnu[PK'\.xNCCform/upgrade.txtnu[PK'\6form/rubric/styles.cssnu[PK'\QC݊݊form/rubric/renderer.phpnu[PK'\Z|OOUform/rubric/db/services.phpnu[PK'\o\form/rubric/db/upgrade.phpnu[PK'\egcform/rubric/db/install.xmlnu[PK'\*rform/rubric/lang/en/gradingform_rubric.phpnu[PK'\(qqEɎform/rubric/backup/moodle2/backup_gradingform_rubric_plugin.class.phpnu[PK'\6Fform/rubric/backup/moodle2/restore_gradingform_rubric_plugin.class.phpnu[PK'\uy+##԰form/rubric/edit_form.phpnu[PK'\OY,form/rubric/pix/icon.svgnu[PK'\4|form/rubric/pix/icon.pngnu[PK'\_vvWform/rubric/lib.phpnu[PK'\'PPform/rubric/version.phpnu[PK'\u"6"6Aform/rubric/classes/grades/grader/gradingpanel/external/fetch.phpnu[PK'\LA:form/rubric/classes/grades/grader/gradingpanel/external/store.phpnu[PK'\. (;form/rubric/classes/privacy/provider.phpnu[PK'\79form/rubric/templates/grades/grader/gradingpanel.mustachenu[PK'\fooform/rubric/preview.phpnu[PK'\$z Hform/rubric/edit.phpnu[PK'\Qh@@Eform/rubric/rubriceditor.phpnu[PK'\$ 1kUform/rubric/amd/src/grades/grader/gradingpanel.jsnu[PK'\=v7`form/rubric/amd/build/grades/grader/gradingpanel.min.jsnu[PK'\! ;fform/rubric/amd/build/grades/grader/gradingpanel.min.js.mapnu[PK'\(W\=\=Dtform/rubric/tests/grades/grader/gradingpanel/external/fetch_test.phpnu[PK'\x x'x'Dform/rubric/tests/grades/grader/gradingpanel/external/store_test.phpnu[PK'\!&xform/rubric/tests/generator/rubric.phpnu[PK'\򭥴Z Z #form/rubric/tests/generator/lib.phpnu[PK'\1 1 )e form/rubric/tests/generator/criterion.phpnu[PK'\"+form/rubric/tests/privacy/provider_test.phpnu[PK'\5s|33$2form/rubric/tests/generator_test.phpnu[PK'\6jUU4fform/rubric/tests/behat/behat_gradingform_rubric.phpnu[PK'\٧  1 form/rubric/tests/behat/reuse_own_rubrics.featurenu[PK'\""+form/rubric/tests/behat/edit_rubric.featurenu[PK'\ 8form/rubric/tests/behat/publish_rubric_templates.featurenu[PK'\C:GG1form/rubric/tests/behat/grade_calculation.featurenu[PK'\#O O /P form/rubric/tests/behat/negative_points.featurenu[PK'\zdM@M@form/rubric/js/rubriceditor.jsnu[PK'\Ψ"HHWform/rubric/js/rubric.jsnu[PK'\!1ޞ)^form/guide/styles.cssnu[PK'\5Z$sform/guide/renderer.phpnu[PK'\B ssform/guide/db/services.phpnu[PK'\tI form/guide/db/upgrade.phpnu[PK'\??<(form/guide/db/install.xmlnu[PK'\󃐮(8form/guide/lang/en/gradingform_guide.phpnu[PK'\hDSform/guide/backup/moodle2/restore_gradingform_guide_plugin.class.phpnu[PK'\HuCXeform/guide/backup/moodle2/backup_gradingform_guide_plugin.class.phpnu[PK'\i#i#vform/guide/edit_form.phpnu[PK'\OYform/guide/pix/icon.svgnu[PK'\v~+Eڜform/guide/pix/plus.svgnu[PK'\ƐG  form/guide/pix/info.svgnu[PK'\4cform/guide/pix/icon.pngnu[PK'\i2F=form/guide/lib.phpnu[PK'\X}@@Tform/guide/version.phpnu[PK'\˫22@Xform/guide/classes/grades/grader/gradingpanel/external/fetch.phpnu[PK'\j@ʋform/guide/classes/grades/grader/gradingpanel/external/store.phpnu[PK'\9qq9  'form/guide/classes/privacy/provider.phpnu[PK'\c8gform/guide/templates/grades/grader/gradingpanel.mustachenu[PK'\~TT-form/guide/templates/comment_chooser.mustachenu[PK'\:form/guide/preview.phpnu[PK'\weG G  form/guide/edit.phpnu[PK'\[form/guide/READMEnu[PK'\b 0form/guide/amd/src/grades/grader/gradingpanel.jsnu[PK'\cOO9form/guide/amd/src/grades/grader/gradingpanel/comments.jsnu[PK'\ pCpform/guide/amd/src/grades/grader/gradingpanel/comments/selectors.jsnu[PK'\ y5GG% form/guide/amd/src/comment_chooser.jsnu[PK'\o6$form/guide/amd/build/grades/grader/gradingpanel.min.jsnu[PK'\[ :!*form/guide/amd/build/grades/grader/gradingpanel.min.js.mapnu[PK'\It?@8form/guide/amd/build/grades/grader/gradingpanel/comments.min.jsnu[PK'\S C<form/guide/amd/build/grades/grader/gradingpanel/comments.min.js.mapnu[PK'\PM#Gform/guide/amd/build/grades/grader/gradingpanel/comments/selectors.min.js.mapnu[PK'\\iIZMform/guide/amd/build/grades/grader/gradingpanel/comments/selectors.min.jsnu[PK'\  /Oform/guide/amd/build/comment_chooser.min.js.mapnu[PK'\ڮ#'+ pform/guide/amd/build/comment_chooser.min.jsnu[PK'\᷐[;[;Cyxform/guide/tests/grades/grader/gradingpanel/external/fetch_test.phpnu[PK'\,G M&M&CGform/guide/tests/grades/grader/gradingpanel/external/store_test.phpnu[PK'\ "form/guide/tests/generator/lib.phpnu[PK'\LZ $.form/guide/tests/generator/guide.phpnu[PK'\5զ (H form/guide/tests/generator/criterion.phpnu[PK'\wm*F form/guide/tests/privacy/provider_test.phpnu[PK'\uH% 8 form/guide/tests/guide_test.phpnu[PK'\6E,,#WF form/guide/tests/generator_test.phpnu[PK'\R‚=os form/guide/tests/behat/grade_listings_and_submissions.featurenu[PK'\uދ 5| form/guide/tests/behat/publish_guide_template.featurenu[PK'\4) form/guide/tests/behat/edit_guide.featurenu[PK'\~Z,Z,2% form/guide/tests/behat/behat_gradingform_guide.phpnu[PK'\w; form/guide/tests/behat/edit_marking_guide_criterion.featurenu[PK'\JƄ 7O form/guide/tests/behat/guide_max_grade_mismatch.featurenu[PK'\O3: form/guide/tests/behat/assign_marking_guide.featurenu[PK'\y8< form/guide/tests/behat/edit_marking_guide_detail.featurenu[PK'\*yޭ73 form/guide/tests/behat/edit_marking_guide_state.featurenu[PK'\#ve3G& form/guide/tests/behat/define_marking_guide.featurenu[PK'\BcWy  37 form/guide/tests/behat/delete_marking_guide.featurenu[PK'\mw]:]:C form/guide/guideeditor.phpnu[PK'\`RR} form/guide/styles.scssnu[PK'\=s|.|.[~ form/guide/js/guideeditor.jsnu[PK'\™: <<# form/guide/js/guide.jsnu[PK'\B\\ lib.phpnu[PK'\T7**  manage.phpnu[PK'\+9 classes/privacy/gradingform_provider_v2.phpnu[PK'\!a@@@ classes/privacy/provider.phpnu[PK'\sWE/% classes/privacy/gradingform_legacy_polyfill.phpnu[PK'\D yui/manage/manage.jsnu[PK'\[,,f pick.phpnu[PK'\ ы tests/generator/lib.phpnu[PK'\a7PP tests/privacy/provider_test.phpnu[PK'\ II& tests/privacy/legacy_polyfill_test.phpnu[PK'\a؞JJV( tests/grading_manager_test.phpnu[PK'\ЋL L = tests/generator_test.phpnu[PK'\p,H tests/behat/behat_grading.phpnu[PK'\*K g pick_form.phpnu[PK\0Ł o message.min.js.mapnu[PK\ эddzy general_action.min.jsnu[PK\sS}  # setmarkingallocation.min.jsnu[PK\[  u setmarkingworkflowstate.min.jsnu[PK\ W** setmarkingallocation.min.js.mapnu[PK\]//[ bulk_actions.min.js.mapnu[PK\i  extend.min.jsnu[PK\\] message.min.jsnu[PK\?Mr general_action.min.js.mapnu[PK\L=hh" setmarkingworkflowstate.min.js.mapnu[PK\?iv" extend.min.js.mapnu[PK\C5dd3 bulk_actions.min.jsnu[PK\W TR delete.min.jsnu[PK\B  f_ delete.min.js.mapnu[PK\!p extra_filters_dropdown.min.js.mapnu[PK\K0n  extra_filters_dropdown.min.jsnu[PK3w