diff --git a/grade/edit/tree/index.php b/grade/edit/tree/index.php index a795705c821..ae874d56f0e 100644 --- a/grade/edit/tree/index.php +++ b/grade/edit/tree/index.php @@ -247,7 +247,7 @@ if (grade_regrade_final_grades_if_required($course, $grade_edit_tree_index_check $actionbar = new \core_grades\output\gradebook_setup_action_bar($context); print_grade_page_head($courseid, 'settings', 'setup', get_string('gradebooksetup', 'grades'), - false, false, true, null, null, null, $actionbar); + false, false, true, null, null, null, $actionbar, false); // Print Table of categories and items echo $OUTPUT->box_start('gradetreebox generalbox'); @@ -292,5 +292,3 @@ $PAGE->requires->js_call_amd('core_form/changechecker', 'watchFormById', ['grade echo $OUTPUT->footer(); die; - - diff --git a/grade/edit/tree/lib.php b/grade/edit/tree/lib.php index 8ac43639766..c93b5c29922 100644 --- a/grade/edit/tree/lib.php +++ b/grade/edit/tree/lib.php @@ -66,6 +66,7 @@ class grade_edit_tree { } $this->columns[] = grade_edit_tree_column::factory('range'); // This is not a setting... How do we deal with it? + $this->columns[] = grade_edit_tree_column::factory('status'); $this->columns[] = grade_edit_tree_column::factory('actions'); if ($this->deepest_level > 1) { @@ -104,7 +105,7 @@ class grade_edit_tree { $object = $element['object']; $eid = $element['eid']; - $object->name = $this->gtree->get_element_header($element, true, false, true, true, true); + $object->name = $this->gtree->get_element_header($element, true, false, true, false, true); $object->icon = $this->gtree->get_element_icon($element); $object->type = $this->gtree->get_element_type_string($element); $object->stripped_name = $this->gtree->get_element_header($element, false, false, false); @@ -119,54 +120,13 @@ class grade_edit_tree { } $moveaction = ''; - $actionsmenu = new action_menu(); - $actionsmenu->set_menu_trigger($OUTPUT->pix_icon('i/moremenu', get_string('actions')), 'actions'); - $actionsmenu->set_owner_selector('grade-item-' . $eid); - - if (!$is_category_item && ($icon = $this->gtree->get_edit_icon($element, $this->gpr, true))) { - $actionsmenu->add($icon); - } - // MDL-49281 if grade_item already has calculation, it should be editable even if global setting is off. - $type = $element['type']; - $iscalculated = ($type == 'item' or $type == 'courseitem' or $type == 'categoryitem') && $object->is_calculated(); - $icon = $this->gtree->get_calculation_icon($element, $this->gpr, true); - if ($iscalculated || $icon) { - $actionsmenu->add($icon); - } + $actions = $this->gtree->get_cell_action_menu($element, 'setup', $this->gpr); if ($element['type'] == 'item' or ($element['type'] == 'category' and $element['depth'] > 1)) { - if ($this->element_deletable($element)) { - $aurl = new moodle_url('index.php', array('id' => $COURSE->id, 'action' => 'delete', 'eid' => $eid, 'sesskey' => sesskey())); - $icon = new action_menu_link_secondary($aurl, new pix_icon('t/delete', get_string('delete')), get_string('delete')); - $actionsmenu->add($icon); - } - - if ($this->element_duplicatable($element)) { - $duplicateparams = array(); - $duplicateparams['id'] = $COURSE->id; - $duplicateparams['action'] = 'duplicate'; - $duplicateparams['eid'] = $eid; - $duplicateparams['sesskey'] = sesskey(); - $aurl = new moodle_url('index.php', $duplicateparams); - $duplicateicon = new pix_icon('t/copy', get_string('duplicate')); - $icon = new action_menu_link_secondary($aurl, $duplicateicon, get_string('duplicate')); - $actionsmenu->add($icon); - } - $aurl = new moodle_url('index.php', array('id' => $COURSE->id, 'action' => 'moveselect', 'eid' => $eid, 'sesskey' => sesskey())); $moveaction .= $OUTPUT->action_icon($aurl, new pix_icon('t/move', get_string('move'))); } - if ($icon = $this->gtree->get_hiding_icon($element, $this->gpr, true)) { - $actionsmenu->add($icon); - } - - if ($icon = $this->gtree->get_reset_icon($element, $this->gpr, true)) { - $actionsmenu->add($icon); - } - - $actions = $OUTPUT->render($actionsmenu); - $returnrows = array(); $root = false; @@ -205,8 +165,6 @@ class grade_edit_tree { $item = $category->get_grade_item(); // Add aggregation coef input if not a course item and if parent category has correct aggregation type - $dimmed = ($item->is_hidden()) ? 'dimmed_text' : ''; - // Before we print the category's row, we must find out how many rows will appear below it (for the filler cell's rowspan) $aggregation_position = grade_get_setting($COURSE->id, 'aggregationposition', $CFG->grade_aggregationposition); $category_total_data = null; // Used if aggregationposition is set to "last", so we can print it last @@ -315,7 +273,7 @@ class grade_edit_tree { $categoryrow = new html_table_row(); $categoryrow->id = 'grade-item-' . $eid; - $categoryrow->attributes['class'] = $courseclass . ' category ' . $dimmed; + $categoryrow->attributes['class'] = $courseclass . ' category '; $categoryrow->attributes['data-category'] = $eid; $categoryrow->attributes['data-itemid'] = $category->get_grade_item()->id; $categoryrow->attributes['data-hidden'] = 'false'; @@ -373,10 +331,9 @@ class grade_edit_tree { $categoryitemclass = 'courseitem'; } - $dimmed = ($item->is_hidden()) ? "dimmed_text" : ""; $gradeitemrow = new html_table_row(); $gradeitemrow->id = 'grade-item-' . $eid; - $gradeitemrow->attributes['class'] = $categoryitemclass . ' item ' . $dimmed; + $gradeitemrow->attributes['class'] = $categoryitemclass . ' item '; $gradeitemrow->attributes['data-itemid'] = $object->id; $gradeitemrow->attributes['data-hidden'] = 'false'; // If this item is a course or category aggregation, add a data attribute that stores the identifier of @@ -486,7 +443,7 @@ class grade_edit_tree { * @param array $element * @return bool */ - function element_deletable($element) { + public static function element_deletable($element) { global $COURSE; if ($element['type'] != 'item') { @@ -514,7 +471,7 @@ class grade_edit_tree { * @param array $element * @return bool */ - public function element_duplicatable($element) { + public static function element_duplicatable($element) { if ($element['type'] != 'item') { return false; } @@ -1010,6 +967,86 @@ class grade_edit_tree_column_range extends grade_edit_tree_column { } } +/** + * Class grade_edit_tree_column_status + * + * @package core_grades + * @copyright 2023 Ilya Tregubov + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class grade_edit_tree_column_status extends grade_edit_tree_column { + + /** + * Get status column header cell + * @return html_table_cell status column header cell + */ + public function get_header_cell() { + $headercell = clone($this->headercell); + $headercell->text = get_string('status'); + return $headercell; + } + + /** + * Get category cell in status column + * + * @param grade_category $category grade category + * @param string $levelclass Category level info + * @param array $params Params (category id, action performed etc) + * @return html_table_cell category cell in status columns + */ + public function get_category_cell($category, $levelclass, $params) { + global $OUTPUT, $gtree; + $categorycell = parent::get_category_cell($category, $levelclass, $params); + $element = []; + $element['object'] = $category; + $categorycell->text = $gtree->set_grade_status_icons($element); + + // Aggregation type. + $aggrstrings = grade_helper::get_aggregation_strings(); + $context = new stdClass(); + $context->aggregation = $aggrstrings[$category->aggregation]; + + // Include/exclude empty grades. + if ($category->aggregateonlygraded) { + $context->aggregateonlygraded = $category->aggregateonlygraded; + } + + // Aggregate outcomes. + if ($category->aggregateoutcomes) { + $context->aggregateoutcomes = $category->aggregateoutcomes; + } + + // Drop the lowest. + if ($category->droplow) { + $context->droplow = $category->droplow; + } + + // Keep the highest. + if ($category->keephigh) { + $context->keephigh = $category->keephigh; + } + $categorycell->text .= $OUTPUT->render_from_template('core_grades/category_settings', $context); + return $categorycell; + } + + /** + * Get category cell in status column + * + * @param grade_item $item grade item + * @param array $params Params + * @return html_table_cell item cell in status columns + */ + public function get_item_cell($item, $params) { + global $gtree; + + $element = []; + $element['object'] = $item; + $itemcell = parent::get_item_cell($item, $params); + $itemcell->text = $gtree->set_grade_status_icons($element); + return $itemcell; + } +} + /** * Class grade_edit_tree_column_actions * diff --git a/grade/lib.php b/grade/lib.php index 54fcfe54319..98840743726 100644 --- a/grade/lib.php +++ b/grade/lib.php @@ -1858,6 +1858,81 @@ class grade_structure { } } + /** + * Returns a link to reset weights for the given element. + * + * @param array $element An array representing an element in the grade_tree + * @param object $gpr A grade_plugin_return object + * @return string|null + */ + public function get_reset_weights_link(array $element, object $gpr): ?string { + + // Limit to category items set to use the natural weights aggregation method, and users + // with the capability to manage grades. + if ($element['type'] != 'category' || $element['object']->aggregation != GRADE_AGGREGATE_SUM || + !has_capability('moodle/grade:manage', $this->context)) { + return null; + } + + $title = grade_helper::get_lang_string('resetweightsshort', 'grades'); + $str = get_string('resetweights', 'grades', $this->get_params_for_iconstr($element)); + $url = new moodle_url('/grade/edit/tree/action.php', [ + 'id' => $this->courseid, + 'action' => 'resetweights', + 'eid' => $element['eid'], + 'sesskey' => sesskey(), + ]); + $gpr->add_url_params($url); + return html_writer::link($url, $title, + ['class' => 'dropdown-item', 'aria-label' => $str, 'role' => 'menuitem']); + } + + /** + * Returns a link to delete a given element. + * + * @param array $element An array representing an element in the grade_tree + * @param object $gpr A grade_plugin_return object + * @return string|null + */ + public function get_delete_link(array $element, object $gpr): ?string { + if ($element['type'] == 'item' || ($element['type'] == 'category' && $element['depth'] > 1)) { + if (grade_edit_tree::element_deletable($element)) { + $url = new moodle_url('index.php', + ['id' => $this->courseid, 'action' => 'delete', 'eid' => $element['eid'], 'sesskey' => sesskey()]); + $title = grade_helper::get_lang_string('delete'); + $gpr->add_url_params($url); + return html_writer::link($url, $title, + ['class' => 'dropdown-item', 'aria-label' => $title, 'role' => 'menuitem']); + } + } + return null; + } + + /** + * Returns a link to duplicate a given element. + * + * @param array $element An array representing an element in the grade_tree + * @param object $gpr A grade_plugin_return object + * @return string|null + */ + public function get_duplicate_link(array $element, object $gpr): ?string { + if ($element['type'] == 'item' || ($element['type'] == 'category' && $element['depth'] > 1)) { + if (grade_edit_tree::element_duplicatable($element)) { + $duplicateparams = []; + $duplicateparams['id'] = $this->courseid; + $duplicateparams['action'] = 'duplicate'; + $duplicateparams['eid'] = $element['eid']; + $duplicateparams['sesskey'] = sesskey(); + $url = new moodle_url('index.php', $duplicateparams); + $title = grade_helper::get_lang_string('duplicate'); + $gpr->add_url_params($url); + return html_writer::link($url, $title, + ['class' => 'dropdown-item', 'aria-label' => $title, 'role' => 'menuitem']); + } + } + return null; + } + /** * Return edit icon for give element * @@ -2384,7 +2459,7 @@ class grade_structure { $context = new stdClass(); - if ($mode == 'gradeitem') { + if ($mode == 'gradeitem' || $mode == 'setup') { $editable = true; if ($element['type'] == 'grade') { @@ -2408,17 +2483,23 @@ class grade_structure { $context->datatype = 'item'; if ($element['type'] == 'item') { - $context = - grade_report::get_additional_context($this->context, $this->courseid, $element, $gpr, $mode, $context, true); - $context->advancedgradingurl = $this->get_advanced_grading_link($element, $gpr); + if ($mode == 'setup') { + $context->deleteurl = $this->get_delete_link($element, $gpr); + $context->duplicateurl = $this->get_duplicate_link($element, $gpr); + } else { + $context = + grade_report::get_additional_context($this->context, $this->courseid, + $element, $gpr, $mode, $context, true); + $context->advancedgradingurl = $this->get_advanced_grading_link($element, $gpr); + } } if ($element['type'] == 'item') { $context->divider1 = true; } - if (!empty($USER->editing)) { - if ($element['type'] !== 'userfield') { + if (!empty($USER->editing) || $mode == 'setup') { + if (($element['type'] !== 'userfield') && ($mode !== 'setup')) { $context->divider1 = true; $context->divider2 = true; } @@ -2464,14 +2545,23 @@ class grade_structure { } } else if ($element['type'] == 'category') { $context->datatype = 'category'; - $mode = 'category'; - $context = grade_report::get_additional_context($this->context, $this->courseid, $element, $gpr, $mode, $context); - if (!empty($USER->editing)) { - $context->divider1 = true; + if ($mode !== 'setup') { + $mode = 'category'; + $context = grade_report::get_additional_context($this->context, $this->courseid, + $element, $gpr, $mode, $context); + } else { + $context->deleteurl = $this->get_delete_link($element, $gpr); + $context->resetweightsurl = $this->get_reset_weights_link($element, $gpr); + } + + if (!empty($USER->editing) || $mode == 'setup') { + if ($mode !== 'setup') { + $context->divider1 = true; + } + $context->editurl = $this->get_edit_link($element, $gpr); + $context->hideurl = $this->get_hiding_link($element, $gpr); + $context->lockurl = $this->get_locking_link($element, $gpr); } - $context->editurl = $this->get_edit_link($element, $gpr); - $context->hideurl = $this->get_hiding_link($element, $gpr); - $context->lockurl = $this->get_locking_link($element, $gpr); } if (isset($element['object'])) { @@ -2487,7 +2577,7 @@ class grade_structure { if (!empty($USER->editing) || isset($context->gradeanalysisurl) || isset($context->gradesonlyurl) || isset($context->aggregatesonlyurl) || isset($context->fullmodeurl) || isset($context->reporturl0) - || isset($context->ascendingfirstnameurl) || isset($context->ascendingurl)) { + || isset($context->ascendingfirstnameurl) || isset($context->ascendingurl) || ($mode == 'setup')) { return $OUTPUT->render_from_template('core_grades/cellmenu', $context); } return ''; @@ -3801,4 +3891,3 @@ abstract class grade_helper { self::$aggregationstrings = null; } } - diff --git a/grade/templates/category_settings.mustache b/grade/templates/category_settings.mustache new file mode 100644 index 00000000000..ceca527f62a --- /dev/null +++ b/grade/templates/category_settings.mustache @@ -0,0 +1,56 @@ +{{! + 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 core_grades/cellmenu + + This template renders action menu for a given cell. + Example context (json): + { + "aggregation": "Natural", + "aggregateonlygraded": 1, + "aggregateoutcomes": 0, + "droplow": 1, + "keephigh": 1 + } +}} +
+ {{#aggregation}} + + {{aggregation}} + + {{/aggregation}} + {{#aggregateonlygraded}} + + {{#str}}aggregateonlygraded, grades{{/str}} + + {{/aggregateonlygraded}} + {{#aggregateoutcomes}} + + {{#str}}aggregateoutcomes, grades{{/str}} + + {{/aggregateoutcomes}} + {{#droplow}} + + {{#str}}droplowestvalues, grades, {{droplow}}{{/str}} + + {{/droplow}} + {{#keephigh}} + + {{#str}}keephigh, grades{{/str}} + + {{/keephigh}} +
diff --git a/grade/templates/cellmenu.mustache b/grade/templates/cellmenu.mustache index 84129ff1eee..ce325154c8b 100644 --- a/grade/templates/cellmenu.mustache +++ b/grade/templates/cellmenu.mustache @@ -48,14 +48,15 @@ diff --git a/grade/tests/behat/behat_grades.php b/grade/tests/behat/behat_grades.php index 202362d6a1b..a9b64aeadc7 100644 --- a/grade/tests/behat/behat_grades.php +++ b/grade/tests/behat/behat_grades.php @@ -159,16 +159,20 @@ class behat_grades extends behat_base { throw new Exception('Unknown item type: ' . $itemtype); } - if ($page == 'grader') { + $xpath = "//table[@id='grade_edit_tree_table']"; + + if (($page == 'grader') || ($page == 'setup')) { + if ($page == 'grader') { + $xpath = "//table[@id='user-grades']"; + } + if ($itemtype == 'gradeitem') { - $xpath = "//table[@id='user-grades']//*[@data-type='item'][@data-id='" . $itemid . "']"; + $xpath .= "//*[@data-type='item'][@data-id='" . $itemid . "']"; } else if (($itemtype == 'category') || ($itemtype == 'course')) { - $xpath = "//table[@id='user-grades']//*[@data-type='category'][@data-id='" . $itemid . "']"; + $xpath .= "//*[@data-type='category'][@data-id='" . $itemid . "']"; } else { throw new Exception('Unknown item type: ' . $itemtype); } - } else if ($page == 'setup') { - $xpath = "//table[@id='grade_edit_tree_table']//*[@data-id='" . $itemid . "']"; } else { throw new Exception('Unknown page: ' . $page); } diff --git a/theme/boost/scss/moodle/grade.scss b/theme/boost/scss/moodle/grade.scss index ff321346012..de73f59ae3e 100644 --- a/theme/boost/scss/moodle/grade.scss +++ b/theme/boost/scss/moodle/grade.scss @@ -372,6 +372,7 @@ .weightoverride { margin-right: 5px; } + min-width: 10em; } &.column-actions { @@ -481,6 +482,14 @@ } } } + + .badge-light { + color: #1d2125; + background-color: #ced4da; + margin-right: 0.5em; + margin-bottom: 0.5em; + } + } } diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index f1f7eb537f0..116f8248c42 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -34867,6 +34867,9 @@ p.arrow_button { width: 18px; height: 18px; } +.path-grade-edit-tree .gradetree-wrapper .setup-grades.generaltable tr td.column-weight { + min-width: 10em; +} .path-grade-edit-tree .gradetree-wrapper .setup-grades.generaltable tr td.column-weight .weightoverride { margin-right: 5px; } @@ -34942,6 +34945,12 @@ p.arrow_button { .path-grade-edit-tree .gradetree-wrapper .setup-grades.generaltable tr.item.categoryitem td:not(.column-actions), .path-grade-edit-tree .gradetree-wrapper .setup-grades.generaltable tr.item.courseitem td:not(.column-actions) { font-weight: bold; } +.path-grade-edit-tree .gradetree-wrapper .badge-light { + color: #1d2125; + background-color: #ced4da; + margin-right: 0.5em; + margin-bottom: 0.5em; +} /** * Grader report. diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 5738ebdcf17..b929f8d2879 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -34867,6 +34867,9 @@ p.arrow_button { width: 18px; height: 18px; } +.path-grade-edit-tree .gradetree-wrapper .setup-grades.generaltable tr td.column-weight { + min-width: 10em; +} .path-grade-edit-tree .gradetree-wrapper .setup-grades.generaltable tr td.column-weight .weightoverride { margin-right: 5px; } @@ -34942,6 +34945,12 @@ p.arrow_button { .path-grade-edit-tree .gradetree-wrapper .setup-grades.generaltable tr.item.categoryitem td:not(.column-actions), .path-grade-edit-tree .gradetree-wrapper .setup-grades.generaltable tr.item.courseitem td:not(.column-actions) { font-weight: bold; } +.path-grade-edit-tree .gradetree-wrapper .badge-light { + color: #1d2125; + background-color: #ced4da; + margin-right: 0.5em; + margin-bottom: 0.5em; +} /** * Grader report.