Merge branch 'MDL-81745-main' of https://github.com/roland04/moodle

This commit is contained in:
Huong Nguyen 2024-11-28 11:36:41 +07:00 committed by Jun Pataleta
commit 4d530b5048
No known key found for this signature in database
GPG Key ID: F83510526D99E2C7
23 changed files with 460 additions and 484 deletions

View File

@ -0,0 +1,5 @@
issueNumber: MDL-81745
notes:
mod_feedback:
- message: Added new `mod_feedback_questions_reorder` external function
type: improved

View File

@ -1,59 +0,0 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Process ajax requests
*
* @copyright Andreas Grabs
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package mod_feedback
*/
if (!defined('AJAX_SCRIPT')) {
define('AJAX_SCRIPT', true);
}
require(__DIR__.'/../../config.php');
require_once('lib.php');
$id = required_param('id', PARAM_INT);
$action = optional_param('action', '', PARAM_ALPHA);
$sesskey = optional_param('sesskey', false, PARAM_TEXT);
$itemorder = optional_param('itemorder', false, PARAM_SEQUENCE);
$cm = get_coursemodule_from_id('feedback', $id, 0, false, MUST_EXIST);
$course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
$feedback = $DB->get_record('feedback', array('id'=>$cm->instance), '*', MUST_EXIST);
require_sesskey();
$context = context_module::instance($cm->id);
require_login($course, true, $cm);
require_capability('mod/feedback:edititems', $context);
$return = false;
switch ($action) {
case 'saveitemorder':
$itemlist = explode(',', trim($itemorder, ','));
if (count($itemlist) > 0) {
$return = feedback_ajax_saveitemorder($itemlist, $feedback);
}
break;
}
echo json_encode($return);
die;

View File

@ -1,3 +1,3 @@
define("mod_feedback/edit",["jquery","core/ajax","core/str","core/notification"],(function($,ajax,str,notification){var manager={deleteItem:function(e){e.preventDefault();var targetUrl=$(e.currentTarget).attr("href");str.get_strings([{key:"confirmation",component:"admin"},{key:"confirmdeleteitem",component:"mod_feedback"},{key:"yes",component:"moodle"},{key:"no",component:"moodle"}]).then((function(s){notification.confirm(s[0],s[1],s[2],s[3],(function(){window.location=targetUrl}))})).catch()},setup:function(){$("body").delegate('[data-action="delete"]',"click",manager.deleteItem)}};return{setup:manager.setup}}));
define("mod_feedback/edit",["exports","core/loadingicon","core/notification","core/pending","core/prefetch","core/sortable_list","core/str","core/toast","mod_feedback/local/repository"],(function(_exports,_loadingicon,_notification,_pending,_prefetch,_sortable_list,_str,_toast,_repository){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_sortable_list=_interopRequireDefault(_sortable_list);const Selectors_deleteQuestionButton='[data-action="delete"]',Selectors_sortableListRegion='[data-region="questions-sortable-list"]',Selectors_sortableElement='[data-region="questions-sortable-list"] .feedback_itemlist[id]',Selectors_sortableElementTitle='[data-region="item-title"]',getItemOrder=element=>{const sortableList=element.closest(Selectors_sortableListRegion);let itemOrder=[];return sortableList.querySelectorAll(Selectors_sortableElement).forEach((item=>{var id;itemOrder.push((id=item.id,Number(id.replace(/^.*feedback_item_/i,""))))})),itemOrder.toString()};let initialized=!1,moduleId=null;_exports.init=cmId=>{if(moduleId=cmId,initialized)return;(0,_prefetch.prefetchStrings)("core",["yes","no"]),(0,_prefetch.prefetchStrings)("admin",["confirmation"]),(0,_prefetch.prefetchStrings)("mod_feedback",["confirmdeleteitem","questionmoved"]),document.addEventListener("click",(async event=>{const deleteButton=event.target.closest(Selectors_deleteQuestionButton);if(deleteButton){event.preventDefault();const confirmationStrings=await(0,_str.getStrings)([{key:"confirmation",component:"admin"},{key:"confirmdeleteitem",component:"mod_feedback"},{key:"yes",component:"core"},{key:"no",component:"core"}]);_notification.default.confirm(...confirmationStrings,(()=>{window.location=deleteButton.getAttribute("href")}))}else;}));new _sortable_list.default(document.querySelector(Selectors_sortableListRegion)).getElementName=element=>{var _element$0$querySelec;return Promise.resolve(null===(_element$0$querySelec=element[0].querySelector(Selectors_sortableElementTitle))||void 0===_element$0$querySelec?void 0:_element$0$querySelec.textContent)},document.addEventListener(_sortable_list.default.EVENTS.elementDrop,(event=>{if(!event.detail.positionChanged)return;const pendingPromise=new _pending.default("mod_feedback/questions:reorder"),itemOrder=getItemOrder(event.detail.element[0]);(0,_loadingicon.addIconToContainerRemoveOnCompletion)(event.detail.element[0],pendingPromise),(0,_repository.reorderQuestions)(moduleId,itemOrder).then((()=>(0,_str.getString)("questionmoved","mod_feedback"))).then(_toast.add).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)})),initialized=!0}}));
//# sourceMappingURL=edit.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
define("mod_feedback/local/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* Module to handle feedback AJAX requests
*
* @module mod_feedback/local/repository
* @copyright 2024 Mikel Martín <mikel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.reorderQuestions=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.reorderQuestions=(moduleId,itemOrder)=>{const request={methodname:"mod_feedback_questions_reorder",args:{cmid:moduleId,itemorder:itemOrder}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=repository.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../../src/local/repository.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Module to handle feedback AJAX requests\n *\n * @module mod_feedback/local/repository\n * @copyright 2024 Mikel Martín <mikel@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Reorder questions for a given feedback course module\n *\n * @param {Number} moduleId\n * @param {String} itemOrder\n * @return {Promise}\n */\nexport const reorderQuestions = (moduleId, itemOrder) => {\n const request = {\n methodname: 'mod_feedback_questions_reorder',\n args: {cmid: moduleId, itemorder: itemOrder}\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["moduleId","itemOrder","request","methodname","args","cmid","itemorder","Ajax","call"],"mappings":";;;;;;;4KAgCgC,CAACA,SAAUC,mBACjCC,QAAU,CACZC,WAAY,iCACZC,KAAM,CAACC,KAAML,SAAUM,UAAWL,mBAG/BM,cAAKC,KAAK,CAACN,UAAU"}

View File

@ -18,48 +18,118 @@
*
* @module mod_feedback/edit
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/str', 'core/notification'],
function($, ajax, str, notification) {
var manager = {
deleteItem: function(e) {
e.preventDefault();
var targetUrl = $(e.currentTarget).attr('href');
str.get_strings([
{
key: 'confirmation',
component: 'admin'
},
{
key: 'confirmdeleteitem',
component: 'mod_feedback'
},
{
key: 'yes',
component: 'moodle'
},
{
key: 'no',
component: 'moodle'
}
])
.then(function(s) {
notification.confirm(s[0], s[1], s[2], s[3], function() {
window.location = targetUrl;
});
"use strict";
return;
})
.catch();
},
import {addIconToContainerRemoveOnCompletion} from 'core/loadingicon';
import Notification from 'core/notification';
import Pending from 'core/pending';
import {prefetchStrings} from 'core/prefetch';
import SortableList from 'core/sortable_list';
import {getString, getStrings} from 'core/str';
import {add as addToast} from 'core/toast';
import {reorderQuestions} from 'mod_feedback/local/repository';
setup: function() {
$('body').delegate('[data-action="delete"]', 'click', manager.deleteItem);
const Selectors = {
deleteQuestionButton: '[data-action="delete"]',
sortableListRegion: '[data-region="questions-sortable-list"]',
sortableElement: '[data-region="questions-sortable-list"] .feedback_itemlist[id]',
sortableElementTitle: '[data-region="item-title"]',
};
/**
* Returns the Feedback question item id from the DOM id of an item.
*
* @param {String} id The dom id, f.g.: feedback_item_22
* @return int
*/
const getItemId = (id) => {
return Number(id.replace(/^.*feedback_item_/i, ''));
};
/**
* Returns the order of the items in the sortable list.
*
* @param {Element} element The element to get the order from.
* @return string
*/
const getItemOrder = (element) => {
const sortableList = element.closest(Selectors.sortableListRegion);
let itemOrder = [];
sortableList.querySelectorAll(Selectors.sortableElement).forEach((item) => {
itemOrder.push(getItemId(item.id));
});
return itemOrder.toString();
};
let initialized = false;
let moduleId = null;
/**
* Initialise editor and all it's modules
*
* @param {Integer} cmId
*/
export const init = (cmId) => {
moduleId = cmId;
// Ensure we only add our listeners once (can be called multiple times).
if (initialized) {
return;
}
prefetchStrings('core', [
'yes',
'no',
]);
prefetchStrings('admin', [
'confirmation',
]);
prefetchStrings('mod_feedback', [
'confirmdeleteitem',
'questionmoved',
]);
// Add event listeners.
document.addEventListener('click', async event => {
// Delete question.
const deleteButton = event.target.closest(Selectors.deleteQuestionButton);
if (deleteButton) {
event.preventDefault();
const confirmationStrings = await getStrings([
{key: 'confirmation', component: 'admin'},
{key: 'confirmdeleteitem', component: 'mod_feedback'},
{key: 'yes', component: 'core'},
{key: 'no', component: 'core'},
]);
Notification.confirm(...confirmationStrings, () => {
window.location = deleteButton.getAttribute('href');
});
return;
}
};
});
return {
setup: manager.setup
};
});
// Initialize sortable list to handle active conditions moving.
const sortableList = new SortableList(document.querySelector(Selectors.sortableListRegion));
sortableList.getElementName = element => Promise.resolve(element[0].querySelector(Selectors.sortableElementTitle)?.textContent);
document.addEventListener(SortableList.EVENTS.elementDrop, event => {
if (!event.detail.positionChanged) {
return;
}
const pendingPromise = new Pending('mod_feedback/questions:reorder');
const itemOrder = getItemOrder(event.detail.element[0]);
addIconToContainerRemoveOnCompletion(event.detail.element[0], pendingPromise);
reorderQuestions(moduleId, itemOrder)
.then(() => getString('questionmoved', 'mod_feedback'))
.then(addToast)
.then(() => pendingPromise.resolve())
.catch(Notification.exception);
});
initialized = true;
};

View File

@ -0,0 +1,40 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Module to handle feedback AJAX requests
*
* @module mod_feedback/local/repository
* @copyright 2024 Mikel Martín <mikel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Reorder questions for a given feedback course module
*
* @param {Number} moduleId
* @param {String} itemOrder
* @return {Promise}
*/
export const reorderQuestions = (moduleId, itemOrder) => {
const request = {
methodname: 'mod_feedback_questions_reorder',
args: {cmid: moduleId, itemorder: itemOrder}
};
return Ajax.call([request])[0];
};

View File

@ -68,8 +68,10 @@ class mod_feedback_complete_form extends moodleform {
$this->structure = $structure;
$this->gopage = isset($customdata['gopage']) ? $customdata['gopage'] : 0;
$isanonymous = $this->structure->is_anonymous() ? ' ianonymous' : '';
parent::__construct(null, $customdata, 'POST', '',
array('id' => $formid, 'class' => 'feedback_form' . $isanonymous), true);
parent::__construct(
customdata: $customdata,
attributes: ['id' => $formid, 'class' => 'feedback_form' . $isanonymous],
);
$this->set_display_vertical();
}
@ -167,10 +169,12 @@ class mod_feedback_complete_form extends moodleform {
* This will add all items to the form, including pagebreaks as horizontal rules.
*/
protected function definition_preview() {
$this->_form->addElement('html', html_writer::start_div('', ['data-region' => 'questions-sortable-list']));
foreach ($this->structure->get_items() as $feedbackitem) {
$itemobj = feedback_get_item_class($feedbackitem->typ);
$itemobj->complete_form_element($feedbackitem, $this);
}
$this->_form->addElement('html', html_writer::end_div());
}
/**
@ -319,6 +323,7 @@ class mod_feedback_complete_form extends moodleform {
$attributes = $element->getAttributes();
$class = !empty($attributes['class']) ? ' ' . $attributes['class'] : '';
$attributes['class'] = $this->get_suggested_class($item) . $class;
$element->setAttributes($attributes);
// Add required rule.
@ -468,13 +473,13 @@ class mod_feedback_complete_form extends moodleform {
$menu->add($action);
}
$editmenu = $OUTPUT->render($menu);
$draghandle = $OUTPUT->render_from_template('core/drag_handle',
['movetitle' => get_string('move_item', 'mod_feedback')]);
$name = $element->getLabel();
$name = html_writer::span('', 'itemdd', array('id' => 'feedback_item_box_' . $item->id)) .
html_writer::span($name, 'itemname') .
html_writer::span($editmenu, 'itemactions');
$element->setLabel(html_writer::span($name, 'itemtitle', ['class' => 'mx-5']));
$name = html_writer::div($draghandle, 'itemhandle', ['data-drag-type' => 'move']) .
html_writer::div($element->getLabel(), 'itemname', ['data-region' => 'item-title']) .
html_writer::div($editmenu, 'itemactions');
$element->setLabel(html_writer::div($name, 'itemtitle d-flex'));
}
/**
@ -580,9 +585,5 @@ class mod_feedback_complete_form extends moodleform {
}
$this->_form->display();
if ($this->mode == self::MODE_EDIT) {
$PAGE->requires->js_call_amd('mod_feedback/edit', 'setup');
}
}
}

View File

@ -0,0 +1,89 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace mod_feedback\external\questions;
use core_external\external_api;
use core_external\external_value;
use core_external\external_function_parameters;
use context_module;
/**
* External method for reordering feedback questions.
*
* @package mod_feedback
* @copyright 2024 Mikel Martín <mikel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class reorder extends external_api {
/**
* Describes the parameters for reorder.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'cmid' => new external_value(PARAM_INT, 'Feedback course module id'),
'itemorder' => new external_value(PARAM_SEQUENCE, 'Feedback order by sequence of question item ids'),
]
);
}
/**
* External function to reorder feedback questions.
*
* @param int $cmid
* @param string $itemorder
* @return bool
*/
public static function execute(int $cmid, string $itemorder): bool {
global $DB;
[
'cmid' => $cmid,
'itemorder' => $itemorder,
] = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'itemorder' => $itemorder,
]);
$cm = get_coursemodule_from_id('feedback', $cmid, 0, false, MUST_EXIST);
$feedback = $DB->get_record('feedback', ['id' => $cm->instance], '*', MUST_EXIST);
$context = context_module::instance($cm->id);
self::validate_context($context);
require_capability('mod/feedback:edititems', $context);
$itemlist = explode(',', trim($itemorder, ',')) ?: [];
if (count($itemlist) > 0) {
return feedback_ajax_saveitemorder($itemlist, $feedback);
}
return false;
}
/**
* Describes the data returned from the external function.
*
* @return external_value
*/
public static function execute_returns(): external_value {
return new external_value(PARAM_BOOL, '', VALUE_REQUIRED);
}
}

View File

@ -141,4 +141,11 @@ $functions = array(
'capabilities' => 'mod/feedback:view',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_feedback_questions_reorder' => [
'classname' => 'mod_feedback\external\questions\reorder',
'description' => 'Saves the new order of the questions in the feedback.',
'type' => 'write',
'ajax' => true,
'capabilities' => 'mod/feedback:edititems',
],
);

View File

@ -86,20 +86,7 @@ $PAGE->activityheader->set_attrs([
'description' => ''
]);
$PAGE->add_body_class('limitedwidth');
//Adding the javascript module for the items dragdrop.
if (count($feedbackitems) > 1) {
$PAGE->requires->strings_for_js([
'pluginname',
'move_item',
'position',
], 'feedback');
$PAGE->requires->yui_module(
'moodle-mod_feedback-dragdrop',
'M.mod_feedback.init_dragdrop',
[['cmid' => $cm->id]]
);
}
$PAGE->requires->js_call_amd('mod_feedback/edit', 'init', [$cm->id]);
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('edit_items', 'mod_feedback'), 3);
@ -109,8 +96,6 @@ $renderer = $PAGE->get_renderer('mod_feedback');
echo $renderer->main_action_bar($actionbar);
$form = new mod_feedback_complete_form(mod_feedback_complete_form::MODE_EDIT,
$feedbackstructure, 'feedback_edit_form');
echo '<div id="feedback_dragarea">'; // The container for the dragging area.
$form->display();
echo '</div>';
echo $OUTPUT->footer();

View File

@ -246,6 +246,7 @@ $string['privacy:metadata:value:value'] = 'The chosen answer.';
$string['privacy:metadata:valuetmp'] = 'A record of the answer to a question in a submission in progress.';
$string['question'] = 'Question';
$string['questionandsubmission'] = 'Question and submission settings';
$string['questionmoved'] = 'Question moved';
$string['questions'] = 'Questions';
$string['questionslimited'] = 'Showing only {$a} first questions, view individual answers or download table data to view all.';
$string['radio'] = 'Multiple choice - single answer';

View File

@ -2867,7 +2867,7 @@ function feedback_page_type_list($pagetype, $parentcontext, $currentcontext) {
/**
* Move save the items of the given $feedback in the order of $itemlist.
* @param string $itemlist a comma separated list with item ids
* @param array $itemlist a list with item ids
* @param stdClass $feedback
* @return bool true if success
*/

View File

@ -237,7 +237,8 @@ class behat_mod_feedback extends behat_base {
public static function get_partial_named_selectors(): array {
return [
new behat_component_named_selector('Question', [
".//*[starts-with(@id, 'fitem_feedback_item_')][.//*[contains(text(), %locator%)]]",
".//*[starts-with(@id, 'fitem_feedback_item_') or starts-with(@id, 'fgroup_feedback_item_')]" .
"[.//*[contains(text(), %locator%)]]",
]),
];
}

View File

@ -151,7 +151,7 @@ Feature: Testing multichoice questions in feedback
# Change the settings so we don't analyse empty submits
And I am on the "Learning experience" "feedback activity" page
And I navigate to "Questions" in current page administration
And I open the action menu in "//div[contains(@class, 'feedback_itemlist') and contains(.,'multichoice1')]" "xpath_element"
And I click on "Edit" "link" in the "this is a multiple choice 1" "mod_feedback > Question"
And I choose "Edit question" in the open action menu
And I set the field "Omit empty submits in analysis" to "Yes"
And I press "Save changes to question"
@ -290,7 +290,7 @@ Feature: Testing multichoice questions in feedback
# Change the settings so we don't analyse empty submits
And I am on the "Learning experience" "feedback activity" page
And I navigate to "Questions" in current page administration
And I open the action menu in "//div[contains(@class, 'feedback_itemlist') and contains(.,'multichoice1')]" "xpath_element"
And I click on "Edit" "link" in the "this is a multiple choice 1" "mod_feedback > Question"
And I choose "Edit question" in the open action menu
And I set the field "Omit empty submits in analysis" to "Yes"
And I press "Save changes to question"
@ -408,7 +408,7 @@ Feature: Testing multichoice questions in feedback
# Change the settings so we don't analyse empty submits
And I am on the "Learning experience" "feedback activity" page
And I navigate to "Questions" in current page administration
And I open the action menu in "//div[contains(@class, 'feedback_itemlist') and contains(.,'multichoice1')]" "xpath_element"
And I click on "Edit" "link" in the "this is a multiple choice 1" "mod_feedback > Question"
And I choose "Edit question" in the open action menu
And I set the field "Omit empty submits in analysis" to "Yes"
And I press "Save changes to question"

View File

@ -17,23 +17,24 @@ Feature: Managing feedback questions
And the following "activities" exist:
| activity | name | course | idnumber |
| feedback | Learning experience course 1 | C1 | feedback1 |
And the following "mod_feedback > question" exists:
| activity | feedback1 |
| name | Is it me you're looking for? |
| label | q1 |
Scenario: Teacher can create a new feedback question
Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher
And I click on "Edit questions" "link" in the "region-main" "region"
When I add a "Short text answer" question to the feedback with:
| Question | Is it me you're looking for? |
| Label | q1 |
Then I should see "(q1) Is it me you're looking for?"
| Question | I can see it in your eyes |
| Label | q2 |
Then I should see "(q2) I can see it in your eyes"
@javascript
Scenario: Teacher can edit feedback questions
Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher
And I click on "Edit questions" "link" in the "region-main" "region"
And I add a "Short text answer" question to the feedback with:
| Question | Is it me you're looking for? |
| Label | q1 |
When I open the action menu in "Is it me you're looking for?" "mod_feedback > Question"
When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question"
And I choose "Edit question" in the open action menu
And I set the field "Question" to "Can you see it in my eyes?"
And I press "Save changes to question"
@ -44,10 +45,7 @@ Feature: Managing feedback questions
Scenario: Teacher can edit and save as new feedback questions
Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher
And I click on "Edit questions" "link" in the "region-main" "region"
And I add a "Short text answer" question to the feedback with:
| Question | Is it me you're looking for? |
| Label | q1 |
When I open the action menu in "Is it me you're looking for?" "mod_feedback > Question"
When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question"
And I choose "Edit question" in the open action menu
And I set the field "Question" to "You can se it in my eyes?"
And I press "Save as new question"
@ -58,10 +56,7 @@ Feature: Managing feedback questions
Scenario: Teacher can delete feedback questions
Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher
And I click on "Edit questions" "link" in the "region-main" "region"
And I add a "Short text answer" question to the feedback with:
| Question | Is it me you're looking for? |
| Label | q1 |
When I open the action menu in "Is it me you're looking for?" "mod_feedback > Question"
When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question"
And I choose "Delete question" in the open action menu
And I click on "Yes" "button" in the "Confirmation" "dialogue"
Then I should not see "(q1) Is it me you're looking for?"
@ -70,18 +65,29 @@ Feature: Managing feedback questions
Scenario: Teacher can mark as required feedback questions
Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher
And I click on "Edit questions" "link" in the "region-main" "region"
And I add a "Short text answer" question to the feedback with:
| Question | Is it me you're looking for? |
| Label | q1 |
| Required | 0 |
When I open the action menu in "Is it me you're looking for?" "mod_feedback > Question"
When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question"
And I choose "Set as required" in the open action menu
And I open the action menu in "Is it me you're looking for?" "mod_feedback > Question"
And I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question"
And I choose "Edit question" in the open action menu
Then the field "Required" matches value "1"
And I press "Cancel"
And I open the action menu in "Is it me you're looking for?" "mod_feedback > Question"
And I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question"
And I choose "Set as not required" in the open action menu
And I open the action menu in "Is it me you're looking for?" "mod_feedback > Question"
And I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question"
And I choose "Edit question" in the open action menu
And the field "Required" matches value "0"
@javascript
Scenario: Teacher can move questions
Given the following "mod_feedback > questions" exist:
| activity | label | name |
| feedback1 | q2 | I can see it in your eyes |
| feedback1 | q3 | I can see it in your smile |
And I am on the "Learning experience course 1" "feedback activity" page logged in as teacher
And I click on "Edit questions" "link" in the "region-main" "region"
When I click on "Move this question" "button" in the "Is it me you're looking for?" "mod_feedback > Question"
Then I should see "After \"(q2) I can see it in your eyes\"" in the "Move this question" "dialogue"
And I should not see "To the top of the list" in the "Move this question" "dialogue"
And I click on "After \"(q3) I can see it in your smile\"" "link" in the "Move this question" "dialogue"
And I click on "Move this question" "button" in the "Is it me you're looking for?" "mod_feedback > Question"
And I click on "To the top of the list" "link" in the "Move this question" "dialogue"

View File

@ -0,0 +1,75 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_feedback\external\questions;
use core_external\external_api;
/**
* Unit tests of external class for re-ordering feedback question items
*
* @package mod_feedback
* @covers \mod_feedback\external\questions\reorder
* @copyright 2024 Mikel Martín <mikel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class reorder_test extends \advanced_testcase {
/**
* Text execute method
*/
public function test_execute(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course with a feedback activity and some questions.
$course = $this->getDataGenerator()->create_course();
$feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]);
$cm = get_coursemodule_from_instance('feedback', $feedback->id);
$feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
$item1 = $feedbackgenerator->create_item_label($feedback);
$item2 = $feedbackgenerator->create_item_info($feedback);
$item3 = $feedbackgenerator->create_item_numeric($feedback);
// Check initial items order.
$this->assertEquals([$item1->id, $item2->id, $item3->id], $this->get_feedback_item_order($feedback));
// Call the execute method to invert the items order.
$result = reorder::execute($cm->id, "$item3->id,$item2->id,$item1->id");
$result = external_api::clean_returnvalue(reorder::execute_returns(), $result);
$this->assertTrue($result);
// Check items order is inverted.
$this->assertEquals([$item3->id, $item2->id, $item1->id], $this->get_feedback_item_order($feedback));
}
/**
* Get the order of the feedback items.
*
* @param object $feedback The feedback activity.
* @return array
*/
private function get_feedback_item_order($feedback) {
global $DB;
return $DB->get_fieldset_select(
'feedback_item',
'id',
'feedback = :feedbackid ORDER BY position ASC',
['feedbackid' => $feedback->id]
);
}
}

View File

@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024100700; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2024100701; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2024100100; // Requires this Moodle version.
$plugin->component = 'mod_feedback'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;

View File

@ -1,265 +0,0 @@
YUI.add('moodle-mod_feedback-dragdrop', function(Y) {
var DRAGDROPNAME = 'mod_feedback_dragdrop';
var CSS = {
DRAGAREA: '#feedback_dragarea',
DRAGITEMCLASS: 'feedback_itemlist',
DRAGITEM: '.row.feedback_itemlist',
DRAGLIST: '#feedback_dragarea form',
DRAGHANDLE: 'itemhandle'
};
var DRAGDROP = function() {
DRAGDROP.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGDROP, M.core.dragdrop, {
initializer : function(params) {
//Static Vars
this.cmid = params.cmid;
this.goingUp = false, lastY = 0;
var groups = ['feedbackitem'];
var handletitle = M.util.get_string('move_item', 'feedback');
//Get the list of li's in the lists and add the drag handle.
basenode = Y.Node.one(CSS.DRAGLIST);
listitems = basenode.all(CSS.DRAGITEM).each(function(v) {
var item_id = this.get_node_id(v.get('id')); //Get the id of the feedback item.
var mydraghandle = this.get_drag_handle(handletitle, CSS.DRAGHANDLE, 'icon');
v.append(mydraghandle); // Insert the new handle into the item box.
}, this);
//We use a delegate to make all items draggable
var del = new Y.DD.Delegate({
container: CSS.DRAGLIST,
nodes: CSS.DRAGITEM,
target: {
padding: '0 0 0 20'
},
handles: ['.' + CSS.DRAGHANDLE],
dragConfig: {groups: groups}
});
//Add plugins to the delegate
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false,
cloneNode: true
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .course-content
constrain: CSS.DRAGAREA
});
del.dd.plug(Y.Plugin.DDWinScroll);
//Listen for all drop:over events
del.on('drop:over', this.drop_over_handler, this);
//Listen for all drag:drag events
del.on('drag:drag', this.drag_drag_handler, this);
//Listen for all drag:start events
del.on('drag:start', this.drag_start_handler, this);
//Listen for a drag:end events
del.on('drag:end', this.drag_end_handler, this);
//Listen for all drag:drophit events
del.on('drag:drophit', this.drag_drophit_handler, this);
//Listen for all drag:dropmiss events
del.on('drag:dropmiss', this.drag_dropmiss_handler, this);
//Create targets for drop.
var droparea = Y.Node.one(CSS.DRAGLIST);
var tar = new Y.DD.Drop({
groups: groups,
node: droparea
});
},
/**
* Handles the drop:over event.
*
* @param e the event
* @return void
*/
drop_over_handler : function(e) {
//Get a reference to our drag and drop nodes
var drag = e.drag.get('node'),
drop = e.drop.get('node');
//Are we dropping on an li node?
if (drop.hasClass(CSS.DRAGITEMCLASS)) {
//Are we not going up?
if (!this.goingUp) {
drop = drop.get('nextSibling');
}
//Add the node to this list
e.drop.get('node').get('parentNode').insertBefore(drag, drop);
//Resize this nodes shim, so we can drop on it later.
e.drop.sizeShim();
}
},
/**
* Handles the drag:drag event.
*
* @param e the event
* @return void
*/
drag_drag_handler : function(e) {
//Get the last y point
var y = e.target.lastXY[1];
//Is it greater than the lastY var?
if (y < this.lastY) {
//We are going up
this.goingUp = true;
} else {
//We are going down.
this.goingUp = false;
}
//Cache for next check
this.lastY = y;
},
/**
* Handles the drag:start event.
*
* @param e the event
* @return void
*/
drag_start_handler : function(e) {
//Get our drag object
var drag = e.target;
//Set some styles here
drag.get('node').addClass('drag_target_active');
drag.get('dragNode').set('innerHTML', drag.get('node').get('innerHTML'));
drag.get('dragNode').addClass('drag_item_active');
drag.get('dragNode').setStyles({
borderColor: drag.get('node').getStyle('borderColor'),
backgroundColor: drag.get('node').getStyle('backgroundColor')
});
},
/**
* Handles the drag:end event.
*
* @param e the event
* @return void
*/
drag_end_handler : function(e) {
var drag = e.target;
//Put our styles back
drag.get('node').removeClass('drag_target_active');
},
/**
* Handles the drag:drophit event.
*
* @param e the event
* @return void
*/
drag_drophit_handler : function(e) {
var drop = e.drop.get('node'),
drag = e.drag.get('node');
dragnode = Y.one(drag);
if (!drop.hasClass(CSS.DRAGITEMCLASS)) {
if (!drop.contains(drag)) {
drop.appendChild(drag);
}
var childElement;
var elementId;
var elements = [];
drop.all(CSS.DRAGITEM).each(function(v) {
childElement = v.one('.felement')?.one('[id^="feedback_item_"]');
if (childElement) {
elementId = this.get_node_id(childElement.get('id'));
if (elements.indexOf(elementId) == -1) {
elements.push(elementId);
}
}
}, this);
var spinner = M.util.add_spinner(Y, dragnode);
this.save_item_order(this.cmid, elements.toString(), spinner);
}
},
/**
* Save the new item order.
*
* @param cmid the coursemodule id
* @param itemorder A comma separated list with item ids
* @param spinner The spinner icon shown while saving
* @return void
*/
save_item_order : function(cmid, itemorder, spinner) {
Y.io(M.cfg.wwwroot + '/mod/feedback/ajax.php', {
//The needed paramaters
data: {action: 'saveitemorder',
id: cmid,
itemorder: itemorder,
sesskey: M.cfg.sesskey
},
timeout: 5000, //5 seconds for timeout I think it is enough.
//Define the events.
on: {
start : function(transactionid) {
spinner.show();
},
success : function(transactionid, xhr) {
var response = xhr.responseText;
var ergebnis = Y.JSON.parse(response);
window.setTimeout(function(e) {
spinner.hide();
}, 250);
require(['core/notification', 'core/str', 'core/toast'], function(Notification, Strings, Toast) {
Strings.get_string('changessaved', 'core').then(function(saveString) {
return Toast.add(saveString);
}).catch(Notification.exception);
});
},
failure : function(transactionid, xhr) {
var msg = {
name : xhr.status+' '+xhr.statusText,
message : xhr.responseText
};
return new M.core.exception(msg);
//~ this.ajax_failure(xhr);
spinner.hide();
}
},
context:this
});
},
/**
* Returns the numeric id from the dom id of an item.
*
* @param id The dom id, f.g.: feedback_item_22
* @return int
*/
get_node_id : function(id) {
return Number(id.replace(/^.*feedback_item_/i, ''));
}
}, {
NAME : DRAGDROPNAME,
ATTRS : {
cmid : {
value : 0
}
}
});
M.mod_feedback = M.mod_feedback || {};
M.mod_feedback.init_dragdrop = function(params) {
return new DRAGDROP(params);
}
}, '@VERSION@', {
requires:['io', 'json-parse', 'dd-constrain', 'dd-proxy', 'dd-drop', 'dd-scroll', 'moodle-core-dragdrop', 'moodle-core-notification']
});

View File

@ -1589,12 +1589,6 @@ $popout-header-height: 4rem;
border: 0;
margin: 0;
}
.drag_target_active {
opacity: .25;
}
.drag_item_active {
opacity: .5;
}
.feedback_bar_image {
height: 10px;
}
@ -1611,39 +1605,44 @@ $popout-header-height: 4rem;
width: 10%;
}
}
.feedback_form {
.itemactions {
display: inline-block;
margin: 0 map-get($spacers, 2);
}
}
.feedback-item-label {
width: 100%;
}
// Feedback edit form.
#feedback_edit_form {
[id*=_feedback_item_].feedback_itemlist {
[id*=_feedback_item_].feedback_itemlist,
.feedback_itemlist.sortable-list-is-dragged {
padding: map-get($spacers, 3);
border: $border-width solid $border-color;
background-color: $white;
position: relative;
@include border-radius();
.itemhandle {
position: absolute;
width: 32px;
height: 32px;
text-align: center;
align-content: center;
.itemname {
margin-right: map-get($spacers, 5);
}
.action-menu {
.itemactions {
position: absolute;
top: 0;
right: 0;
.dropdown-toggle {
@include border-radius(.5rem);
width: $icon-medium-width;
height: $icon-medium-height;
}
}
.dropdown-toggle {
border-radius: .5rem;
width: 32px;
height: 32px;
&.sortable-list-current-position {
background-color: $light;
}
}
.sortable-list-is-dragged {
opacity: .75;
max-width: $course-content-maxwidth;
}
.loading-icon {
position: absolute;
left: 50%;
top: calc(50% - .5rem);
}
}
// Templates page.

View File

@ -35062,12 +35062,6 @@ img.userpicture {
border: 0;
margin: 0;
}
.path-mod-feedback .drag_target_active {
opacity: 0.25;
}
.path-mod-feedback .drag_item_active {
opacity: 0.5;
}
.path-mod-feedback .feedback_bar_image {
height: 10px;
}
@ -35080,35 +35074,46 @@ img.userpicture {
.path-mod-feedback .templateslist th.header.action {
width: 10%;
}
.path-mod-feedback .feedback_form .itemactions {
display: inline-block;
margin: 0 0.5rem;
}
.path-mod-feedback .feedback-item-label {
width: 100%;
}
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist {
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist,
.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged {
padding: 1rem;
border: 1px solid #dee2e6;
background-color: #fff;
position: relative;
border-radius: 0.5rem;
}
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemhandle {
position: absolute;
width: 32px;
height: 32px;
text-align: center;
align-content: center;
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemname,
.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemname {
margin-right: 2rem;
}
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .action-menu {
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemactions,
.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemactions {
position: absolute;
top: 0;
right: 0;
}
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .dropdown-toggle {
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemactions .dropdown-toggle,
.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemactions .dropdown-toggle {
border-radius: 0.5rem;
width: 32px;
height: 32px;
}
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist.sortable-list-current-position,
.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged.sortable-list-current-position {
background-color: #f8f9fa;
}
.path-mod-feedback #feedback_edit_form .sortable-list-is-dragged {
opacity: 0.75;
max-width: 830px;
}
.path-mod-feedback #feedback_edit_form .loading-icon {
position: absolute;
left: 50%;
top: calc(50% - 0.5rem);
}
.path-mod-feedback#page-mod-feedback-manage_templates .coursetemplates .no-overflow,
.path-mod-feedback#page-mod-feedback-manage_templates .publictemplates .no-overflow {
overflow: visible;

View File

@ -35062,12 +35062,6 @@ img.userpicture {
border: 0;
margin: 0;
}
.path-mod-feedback .drag_target_active {
opacity: 0.25;
}
.path-mod-feedback .drag_item_active {
opacity: 0.5;
}
.path-mod-feedback .feedback_bar_image {
height: 10px;
}
@ -35080,35 +35074,46 @@ img.userpicture {
.path-mod-feedback .templateslist th.header.action {
width: 10%;
}
.path-mod-feedback .feedback_form .itemactions {
display: inline-block;
margin: 0 0.5rem;
}
.path-mod-feedback .feedback-item-label {
width: 100%;
}
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist {
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist,
.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged {
padding: 1rem;
border: 1px solid #dee2e6;
background-color: #fff;
position: relative;
border-radius: 0.25rem;
}
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemhandle {
position: absolute;
width: 32px;
height: 32px;
text-align: center;
align-content: center;
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemname,
.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemname {
margin-right: 2rem;
}
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .action-menu {
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemactions,
.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemactions {
position: absolute;
top: 0;
right: 0;
}
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .dropdown-toggle {
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemactions .dropdown-toggle,
.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemactions .dropdown-toggle {
border-radius: 0.5rem;
width: 32px;
height: 32px;
}
.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist.sortable-list-current-position,
.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged.sortable-list-current-position {
background-color: #f8f9fa;
}
.path-mod-feedback #feedback_edit_form .sortable-list-is-dragged {
opacity: 0.75;
max-width: 830px;
}
.path-mod-feedback #feedback_edit_form .loading-icon {
position: absolute;
left: 50%;
top: calc(50% - 0.5rem);
}
.path-mod-feedback#page-mod-feedback-manage_templates .coursetemplates .no-overflow,
.path-mod-feedback#page-mod-feedback-manage_templates .publictemplates .no-overflow {
overflow: visible;