Merge branch 'MDL-80747-main' of https://github.com/rezaies/moodle

This commit is contained in:
Huong Nguyen
2024-08-26 10:05:06 +07:00
16 changed files with 212 additions and 69 deletions
@@ -0,0 +1,9 @@
issueNumber: MDL-80747
notes:
core:
- message: >
The core\output\select_menu widget now supports rendering dividers
between menu options. Empty elements (null or empty strings) within the
array of options are considered and rendered as dividers in the dropdown
menu.
type: improved
@@ -0,0 +1,9 @@
issueNumber: MDL-80747
notes:
core:
- message: >
The `core\output\select_menu` widget now supports a new feature: inline
labels. You can render the label inside the combobox widget by passing
`true` to the `$inlinelabel` parameter when calling the `->set_label()`
method.
type: improved
@@ -53,6 +53,14 @@ class behat_form_select_menu extends behat_form_field {
return $input->getValue();
}
public function matches($expectedvalue) {
$actualvalue = $this->get_value();
$selectedcontainer = $this->field->find('css', '[data-selected-option]');
$actualtext = $selectedcontainer ? $selectedcontainer->getText() : $this->field->getText();
return ($expectedvalue == $actualvalue || $expectedvalue == $actualtext);
}
/**
* Checks whether a given option exists in the select menu field.
*
+28 -14
View File
@@ -39,6 +39,9 @@ class select_menu implements renderable, templatable {
/** @var array Button label's attributes */
protected $labelattributes = [];
/** @var bool Whether the label is inline or not */
protected $inlinelabel = false;
/** @var string Name of the combobox element */
protected $name;
@@ -47,7 +50,7 @@ class select_menu implements renderable, templatable {
*
* @param string $name Name of the combobox element
* @param array $options List of options in an associative array format like ['val' => 'Option'].
* Supports grouped options as well.
* Supports grouped options as well. Empty string or null values will be rendered as dividers.
* @param string|null $selected The value of the preselected option.
*/
public function __construct(string $name, array $options, ?string $selected = null) {
@@ -61,10 +64,12 @@ class select_menu implements renderable, templatable {
*
* @param string $label The label.
* @param array $attributes List of attributes to apply on the label element.
* @param bool $inlinelabel Whether the label is inline or not.
*/
public function set_label(string $label, array $attributes = []) {
public function set_label(string $label, array $attributes = [], bool $inlinelabel = false) {
$this->label = $label;
$this->labelattributes = $attributes;
$this->inlinelabel = $inlinelabel;
}
/**
@@ -87,21 +92,29 @@ class select_menu implements renderable, templatable {
];
}
foreach ($optoptions as $optvalue => $optoption) {
$flattened[$groupname]['options'][$optvalue] = [
'name' => $optoption,
'value' => $optvalue,
'selected' => $this->selected == $optvalue,
'id' => \html_writer::random_id('select-menu-option'),
];
if (empty($optoption)) {
$flattened[$groupname]['options'][$optvalue] = ['isdivider' => true];
} else {
$flattened[$groupname]['options'][$optvalue] = [
'name' => $optoption,
'value' => $optvalue,
'selected' => $this->selected == $optvalue,
'id' => \html_writer::random_id('select-menu-option'),
];
}
}
}
} else {
$flattened[$value] = [
'name' => $option,
'value' => $value,
'selected' => $this->selected == $value,
'id' => \html_writer::random_id('select-menu-option'),
];
if (empty($option)) {
$flattened[$value] = ['isdivider' => true];
} else {
$flattened[$value] = [
'name' => $option,
'value' => $value,
'selected' => $this->selected == $value,
'id' => \html_writer::random_id('select-menu-option'),
];
}
}
}
@@ -153,6 +166,7 @@ class select_menu implements renderable, templatable {
$data = new \stdClass();
$data->baseid = \html_writer::random_id('select-menu');
$data->label = $this->label;
$data->inlinelabel = $this->inlinelabel;
$data->options = $this->flatten_options();
$data->selectedoption = $this->get_selected_option();
$data->name = $this->name;
+32 -10
View File
@@ -85,11 +85,11 @@
}
}}
<div class="dropdown select-menu" id="{{baseid}}">
{{#label}}
{{#label}}{{^inlinelabel}}
<label id="{{baseid}}-label"{{#labelattributes}} {{name}}="{{value}}"{{/labelattributes}}>{{label}}</label>
{{/label}}
{{/inlinelabel}}{{/label}}
<div
class="btn dropdown-toggle"
class="btn dropdown-toggle{{#inlinelabel}} d-flex text-left align-items-center p-0{{/inlinelabel}}"
role="combobox"
data-toggle="dropdown"
{{#label}}aria-labelledby="{{baseid}}-label"{{/label}}
@@ -99,7 +99,19 @@
data-input-element="{{baseid}}-input"
tabindex="0"
>
{{selectedoption}}
{{#inlinelabel}}
<div class="pr-3 text-truncate">
{{#label}}
<label class="d-block m-0 small" id="{{baseid}}-label"{{#labelattributes}} {{name}}="{{value}}"{{/labelattributes}}>{{label}}</label>
{{/label}}
<span class="font-weight-bold" data-selected-option>
{{selectedoption}}
</span>
</div>
{{/inlinelabel}}
{{^inlinelabel}}
{{selectedoption}}
{{/inlinelabel}}
</div>
<ul class="dropdown-menu" role="listbox" id="{{baseid}}-listbox" {{#label}}aria-labelledby="{{baseid}}-label"{{/label}}>
{{#options}}
@@ -108,17 +120,27 @@
<ul role="group" aria-labelledby="{{id}}">
<li role="presentation" id="{{id}}">{{name}}</li>
{{#options}}
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}" {{#selected}}aria-selected="true"{{/selected}}>
{{name}}
</li>
{{#isdivider}}
<li role="separator" class="dropdown-divider"></li>
{{/isdivider}}
{{^isdivider}}
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}" {{#selected}}aria-selected="true"{{/selected}}>
{{name}}
</li>
{{/isdivider}}
{{/options}}
</ul>
</li>
{{/isgroup}}
{{^isgroup}}
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}" {{#selected}}aria-selected="true"{{/selected}}>
{{name}}
</li>
{{#isdivider}}
<li role="separator" class="dropdown-divider"></li>
{{/isdivider}}
{{^isdivider}}
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}" {{#selected}}aria-selected="true"{{/selected}}>
{{name}}
</li>
{{/isdivider}}
{{/isgroup}}
{{/options}}
</ul>
@@ -63,15 +63,6 @@ class grading_options_temp_form extends \moodleform {
}
}
$mform->addElement('select', 'perpage', get_string('assignmentsperpage', 'assign'), $options);
$options = array('' => get_string('filternone', 'assign'),
ASSIGN_FILTER_NOT_SUBMITTED => get_string('filternotsubmitted', 'assign'),
ASSIGN_FILTER_DRAFT => get_string('filterdraft', 'assign'),
ASSIGN_FILTER_SUBMITTED => get_string('filtersubmitted', 'assign'),
ASSIGN_FILTER_REQUIRE_GRADING => get_string('filterrequiregrading', 'assign'),
ASSIGN_FILTER_GRANTED_EXTENSION => get_string('filtergrantedextension', 'assign'));
if ($instance['submissionsenabled']) {
$mform->addElement('select', 'filter', get_string('filter', 'assign'), $options);
}
if (!empty($instance['markingallocationopt'])) {
$markingfilter = get_string('markerfilter', 'assign');
$mform->addElement('select', 'markerfilter', $markingfilter, $instance['markingallocationopt']);
@@ -113,16 +113,28 @@ class grading_actionmenu implements templatable, renderable {
$data['extrafiltersdropdown'] = $OUTPUT->render($extrafiltersdropdown);
}
if (groups_get_activity_group($cm) || $this->get_applied_extra_filters_count() > 0) {
$activitygroup = groups_get_activity_group($cm);
$hasuserfilter = get_user_preferences('assign_filter');
$hasextrafilters = $this->get_applied_extra_filters_count() > 0;
if ($activitygroup || $hasuserfilter || $hasextrafilters) {
$url = new moodle_url('/mod/assign/view.php', [
'id' => $this->cmid,
'action' => 'grading',
'group' => 0,
'status' => '',
'workflowfilter' => '',
'sesskey' => sesskey(),
]);
$data['pagereset'] = $url->out(false);
}
if ($this->assign->is_any_submission_plugin_enabled()) {
['statusmenu' => $statusmenu, 'currentvalue' => $currentvalue] = $this->get_status_menu();
$statusselect = new \core\output\select_menu('status', $statusmenu, $currentvalue);
$statusselect->set_label(get_string('status', 'mod_assign'), [], true);
$data['statusselector'] = $statusselect->export_for_template($output);
}
if ($this->assign->can_grade()) {
$url = new moodle_url('/mod/assign/view.php', [
'id' => $this->assign->get_course_module()->id,
@@ -199,6 +211,45 @@ class grading_actionmenu implements templatable, renderable {
return $actions;
}
/**
* Get the status menu for the grading action menu.
*
* @return array An array containing the status menu and the current value.
*/
private function get_status_menu(): array {
$statusmenu = [];
$currentvalue = '';
$groupedfilters = $this->assign->get_filters(true);
foreach ($groupedfilters as $group => $filters) {
foreach ($filters as $filter) {
if ($filter['key'] === 'none') {
// The 'none' filter is not a real filter.
$filter['key'] = '';
}
$url = new moodle_url('/mod/assign/view.php', [
'id' => $this->assign->get_course_module()->id,
'action' => 'grading',
'sesskey' => sesskey(),
'status' => $filter['key'],
]);
$statusmenu[$url->out(false)] = $filter['name'];
if ($filter['active']) {
$currentvalue = $url->out(false);
}
}
if ($group !== array_key_last($groupedfilters)) {
$statusmenu[] = '';
}
}
return [
'statusmenu' => $statusmenu,
'currentvalue' => $currentvalue,
];
}
/**
* The renderable for the extra filters dropdown, if available.
*
+1
View File
@@ -293,6 +293,7 @@ $string['feedbacksettings'] = 'Feedback settings';
$string['feedbacktypes'] = 'Feedback types';
$string['filesubmissions'] = 'File submissions';
$string['filter'] = 'Filter';
$string['filterall'] = 'All';
$string['filterdraft'] = 'Draft';
$string['filtergrantedextension'] = 'Granted extension';
$string['filternone'] = 'No filter';
+36 -23
View File
@@ -4484,6 +4484,12 @@ class assign {
require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
require_once($CFG->dirroot . '/mod/assign/quickgradingform.php');
require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
$submittedfilter = optional_param('status', null, PARAM_ALPHA);
if (isset($submittedfilter) && confirm_sesskey()) {
set_user_preference('assign_filter', $submittedfilter);
}
$o = '';
$cmid = $this->get_course_module()->id;
@@ -4570,7 +4576,6 @@ class assign {
$gradingoptionsdata = new stdClass();
$gradingoptionsdata->perpage = $perpage;
$gradingoptionsdata->filter = $filter;
$gradingoptionsdata->markerfilter = $markerfilter;
$gradingoptionsform->set_data($gradingoptionsdata);
@@ -7422,9 +7427,6 @@ class assign {
$mform = new mod_assign\form\grading_options_temp_form(null, $gradingoptionsparams);
if ($formdata = $mform->get_data()) {
set_user_preference('assign_perpage', $formdata->perpage);
if (isset($formdata->filter)) {
set_user_preference('assign_filter', $formdata->filter);
}
if (isset($formdata->markerfilter)) {
set_user_preference('assign_markerfilter', $formdata->markerfilter);
}
@@ -9659,35 +9661,46 @@ class assign {
/**
* Return array of valid search filters for the grading interface.
*
* @param bool $grouped Whether to return the filters grouped or not.
* @return array
*/
public function get_filters() {
$filterkeys = [
ASSIGN_FILTER_NOT_SUBMITTED,
ASSIGN_FILTER_DRAFT,
ASSIGN_FILTER_SUBMITTED,
ASSIGN_FILTER_REQUIRE_GRADING,
ASSIGN_FILTER_GRANTED_EXTENSION
public function get_filters(bool $grouped = false): array {
$groupedfilterkeys = [
[
ASSIGN_FILTER_NOT_SUBMITTED,
ASSIGN_FILTER_DRAFT,
ASSIGN_FILTER_SUBMITTED,
ASSIGN_FILTER_REQUIRE_GRADING,
],
[
ASSIGN_FILTER_GRANTED_EXTENSION,
],
];
$current = get_user_preferences('assign_filter', '');
$filters = [];
// First is always "no filter" option.
array_push($filters, [
'key' => 'none',
'name' => get_string('filternone', 'assign'),
'active' => ($current == '')
]);
$filters[0] = [
[
'key' => 'none',
'name' => get_string('filterall', 'assign'),
'active' => ($current == ''),
],
];
foreach ($filterkeys as $key) {
array_push($filters, [
'key' => $key,
'name' => get_string('filter' . $key, 'assign'),
'active' => ($current == $key)
]);
foreach ($groupedfilterkeys as $group => $filterkeys) {
foreach ($filterkeys as $key) {
$filters[$group] = $filters[$group] ?? [];
$filters[$group][] = [
'key' => $key,
'name' => get_string('filter' . $key, 'assign'),
'active' => ($current == $key),
];
}
}
return $filters;
return $grouped ? $filters : array_merge(...$filters);
}
/**
-6
View File
@@ -131,12 +131,6 @@ M.mod_assign.init_grading_options = function(Y) {
paginationelement.on('change', function(e) {
Y.one('form.gradingoptionsform').submit();
});
var filterelement = Y.one('#id_filter');
if (filterelement) {
filterelement.on('change', function(e) {
Y.one('form.gradingoptionsform').submit();
});
}
var markerfilterelement = Y.one('#id_markerfilter');
if (markerfilterelement) {
markerfilterelement.on('change', function(e) {
@@ -25,6 +25,7 @@
Context variables required for this template:
* userselector - HTML that outputs the user selector
* groupselector - (optional) HTML that outputs the group selector
* statusselector - (optional) The data object containing the required properties to render the status selector
* extrafiltersdropdown - HTML that outputs the extra filters dropdown
* actions - (optional) HTML that outputs the bulk action menu
@@ -32,6 +33,20 @@
{
"userselector": "<div class='user-search'></div>",
"groupselector": "<div class='group-selector'></div>",
"statusselector": {
"name": "filter",
"value": "submitted",
"baseid": "select-menu56789",
"label": "Status",
"inlinelabel": true,
"selectedoption": "Not submitted",
"options": [
{"name": "All", "value": "", "selected": false, "id": "select-menu-option56789a"},
{"name": "Not submitted", "value": "notsubmitted", "selected": true, "id": "select-menu-option56789b"},
{"isdivider": true},
{"name": "granted extention", "value": "grantedextension", "selected": false, "id": "select-menu-option56789d"}
]
},
"extrafiltersdropdown": "<div class='dropdown extrafilters'></div>",
"pagereset": "http://moodle.local/mod/assign/view.php?id=2&action=grading&group=0",
"actions": "<div class='action-menu'></div>"
@@ -56,6 +71,17 @@
</div>
<div class="navitem-divider d-none d-sm-flex"></div>
{{/groupselector}}
{{#statusselector}}
<div class="navitem">
{{>core/select_menu}}
</div>
<div class="navitem-divider d-none d-sm-flex"></div>
{{#js}}
document.querySelector('#{{baseid}}').addEventListener('change', function(e) {
window.location.href = e.target.value;
});
{{/js}}
{{/statusselector}}
{{#extrafiltersdropdown}}
<div class="navitem">
{{{.}}}
+1 -1
View File
@@ -43,7 +43,7 @@ Feature: In an assignment, teachers can filter displayed submissions and see dra
Scenario: View assignments with draft status on the view all submissions page
Given I am on the "Test assignment" Activity page logged in as teacher1
And I navigate to "Submissions" in current page administration
When I set the field "Filter" to "Draft"
When I set the field "Status" to "Draft"
Then I should see "Student 2"
And I should not see "Student 1"
And I should not see "Student 3"
@@ -47,7 +47,7 @@ Feature: In an assignment, teachers can change filters in the grading app
And I am on the "Test assignment name &" Activity page
And I navigate to "Submissions" in current page administration
And I set the field "filter" to "Not submitted"
And I set the field "Status" to "Not submitted"
And I set the field "markerfilter" to "Marker 1"
And I click on "Advanced" "button" in the ".tertiary-navigation" "css_element"
And I set the field "Marking state" in the ".extrafilters .dropdown-menu" "css_element" to "In marking"
@@ -77,7 +77,7 @@ Feature: In an assignment, teachers can change filters in the grading app
And I set the field "markerfilter" to "Marker 1"
And I set the field "workflowfilter" to "In marking"
And I click on "View all submissions" "link"
Then the field "filter" matches value "Not submitted"
Then the field "Status" matches value "Not submitted"
And the field "markerfilter" matches value "Marker 1"
And I click on "Advanced" "button" in the ".tertiary-navigation" "css_element"
And the field "Marking state" matches value "In marking"
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -1
View File
@@ -380,7 +380,12 @@ const comboboxFix = () => {
if (combobox.hasAttribute('value')) {
combobox.value = option.dataset.shortText || option.textContent.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
} else {
combobox.textContent = option.dataset.shortText || option.textContent;
const selectedOptionContainer = combobox.querySelector('[data-selected-option]');
if (selectedOptionContainer) {
selectedOptionContainer.textContent = option.dataset.shortText || option.textContent;
} else {
combobox.textContent = option.dataset.shortText || option.textContent;
}
}
if (combobox.dataset.inputElement) {