MDL-76713 core_availability: Fix date restriction conflict

* Use a unique ID the date nodes in the HTML tree to be able to find the current node.
* Look for nodes in the same tree "leaf" and work on conflicts in this single leaf/branch.
This commit is contained in:
Laurent David 2023-04-05 15:13:02 +02:00 committed by Laurent David
parent 6988b5b328
commit 4f620f537e
11 changed files with 394 additions and 132 deletions

View File

@ -0,0 +1,105 @@
@availability @availability_date @javascript
Feature: As a teacher I can set availability dates restriction to an activity and see a warning when conflicting dates are set
Background:
Given the following "courses" exist:
| fullname | shortname | format | enablecompletion |
| Course 1 | C1 | topics | 1 |
And the following "users" exist:
| username |
| teacher1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | introformat | course | content | contentformat | idnumber |
| page | PageName1 | PageDesc1 | 1 | C1 | Page 1 | 1 | 1 |
Scenario: When I set dates to potential conflicting dates in the same subset, I should see a warning.
Given I am on the PageName1 "page activity editing" page logged in as teacher1
And I expand all fieldsets
And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "year" in the "1" "availability_date > Date Restriction" to "2023"
And I set the field "Month" in the "1" "availability_date > Date Restriction" to "April"
And I set the field "day" in the "1" "availability_date > Date Restriction" to "4"
And I set the field "Direction" in the "1" "availability_date > Date Restriction" to "from"
And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "year" in the "2" "availability_date > Date Restriction" to "2023"
And I set the field "Month" in the "2" "availability_date > Date Restriction" to "April"
And I set the field "day" in the "2" "availability_date > Date Restriction" to "6"
And I set the field "Direction" in the "2" "availability_date > Date Restriction" to "until"
And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "year" in the "3" "availability_date > Date Restriction" to "2023"
And I set the field "Month" in the "3" "availability_date > Date Restriction" to "April"
And I set the field "day" in the "3" "availability_date > Date Restriction" to "6"
When I set the field "Direction" in the "3" "availability_date > Date Restriction" to "from"
Then I should see "Conflicts with other date restrictions"
Scenario: If there are conflicting dates in the same subset, I should not see a warning if condition are separated by "any".
Given I am on the PageName1 "page activity editing" page logged in as teacher1
And I expand all fieldsets
And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area"
And I click on "Restriction set" "button" in the "Add restriction..." "dialogue"
And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "year" in the "1.1" "availability_date > Date Restriction" to "2023"
And I set the field "Month" in the "1.1" "availability_date > Date Restriction" to "April"
And I set the field "day" in the "1.1" "availability_date > Date Restriction" to "4"
And I set the field "Direction" in the "1.1" "availability_date > Date Restriction" to "from"
And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "year" in the "1.2" "availability_date > Date Restriction" to "2023"
And I set the field "Month" in the "1.2" "availability_date > Date Restriction" to "April"
And I set the field "day" in the "1.2" "availability_date > Date Restriction" to "6"
And I set the field "Direction" in the "1.2" "availability_date > Date Restriction" to "until"
And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "year" in the "1.3" "availability_date > Date Restriction" to "2023"
And I set the field "Month" in the "1.3" "availability_date > Date Restriction" to "April"
And I set the field "day" in the "1.3" "availability_date > Date Restriction" to "6"
And I set the field "Direction" in the "1.3" "availability_date > Date Restriction" to "from"
When I set the field "Required restrictions" in the "1" "core_availability > Set Of Restrictions" to "any"
Then I should not see "Conflicts with other date restrictions"
Scenario: There should a conflicting availability dates are in the same subset separated by "all".
Given I am on the PageName1 "page activity editing" page logged in as teacher1
And I expand all fieldsets
# Root level: Student "must" match the following.
And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area"
And I click on "Restriction set" "button" in the "Add restriction..." "dialogue"
# This is the second level: Student "must" match any of the following.
And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area"
And I click on "Restriction set" "button" in the "Add restriction..." "dialogue"
# And now the third and final level.
And I click on "Add restriction..." "button" in the "1.1" "core_availability > Availability Button Area"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "year" in the "1.1.1" "availability_date > Date Restriction" to "2023"
And I set the field "Month" in the "1.1.1" "availability_date > Date Restriction" to "April"
And I set the field "day" in the "1.1.1" "availability_date > Date Restriction" to "2"
And I set the field "Direction" in the "1.1.1" "availability_date > Date Restriction" to "from"
And I click on "Add restriction..." "button" in the "1.1" "core_availability > Availability Button Area"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "year" in the "1.1.2" "availability_date > Date Restriction" to "2023"
And I set the field "Month" in the "1.1.2" "availability_date > Date Restriction" to "April"
And I set the field "day" in the "1.1.2" "availability_date > Date Restriction" to "3"
And I set the field "Direction" in the "1.1.2" "availability_date > Date Restriction" to "until"
# Then add a restriction to the second level.
And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area"
And I click on "Restriction set" "button" in the "Add restriction..." "dialogue"
And I click on "Add restriction..." "button" in the "1.2" "core_availability > Availability Button Area"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "year" in the "1.2.1" "availability_date > Date Restriction" to "2023"
And I set the field "Month" in the "1.2.1" "availability_date > Date Restriction" to "April"
And I set the field "day" in the "1.2.1" "availability_date > Date Restriction" to "4"
And I set the field "Direction" in the "1.2.1" "availability_date > Date Restriction" to "from"
And I click on "Add restriction..." "button" in the "1.2" "core_availability > Availability Button Area"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "year" in the "1.2.2" "availability_date > Date Restriction" to "2023"
And I set the field "Month" in the "1.2.2" "availability_date > Date Restriction" to "April"
And I set the field "day" in the "1.2.2" "availability_date > Date Restriction" to "3"
When I set the field "Direction" in the "1.2.2" "availability_date > Date Restriction" to "until"
# Same subset, we can detect conflicts.
Then I should see "Conflicts with other date restrictions"

View File

@ -0,0 +1,39 @@
<?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/>.
use Behat\Mink\Element\NodeElement;
/**
* Behat availabilty-related steps definitions.
*
* @package availability_date
* @category test
* @copyright 2023 Laurent David <laurent.david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_availability_date extends behat_base {
/**
* Return the list of partial named selectors.
*
* @return array
*/
public static function get_partial_named_selectors(): array {
return [
new behat_component_named_selector(
'Date Restriction', ["//div[h3[@data-restriction-order=%locator% and contains(text(), 'Date restriction')]]"]
),
];
}
}

View File

@ -65,6 +65,11 @@ M.availability_date.form.getNode = function(json) {
// Set default time that corresponds to the HTML selectors. // Set default time that corresponds to the HTML selectors.
node.setData('time', this.defaultTime); node.setData('time', this.defaultTime);
} }
if (json.nodeUID === undefined) {
var miliTime = new Date();
json.nodeUID = miliTime.getTime();
}
node.setData('nodeUID', json.nodeUID);
if (json.d !== undefined) { if (json.d !== undefined) {
node.one('select[name=direction]').set('value', json.d); node.one('select[name=direction]').set('value', json.d);
} }
@ -114,7 +119,7 @@ M.availability_date.form.getNode = function(json) {
* gets an AJAX response. * gets an AJAX response.
* *
* @method updateTime * @method updateTime
* @param {Y.Node} component Node for plugin controls * @param {Y.Node} node Node for plugin controls
*/ */
M.availability_date.form.updateTime = function(node) { M.availability_date.form.updateTime = function(node) {
// After a change to the date/time we need to recompute the // After a change to the date/time we need to recompute the
@ -140,39 +145,53 @@ M.availability_date.form.updateTime = function(node) {
M.availability_date.form.fillValue = function(value, node) { M.availability_date.form.fillValue = function(value, node) {
value.d = node.one('select[name=direction]').get('value'); value.d = node.one('select[name=direction]').get('value');
value.t = parseInt(node.getData('time'), 10); value.t = parseInt(node.getData('time'), 10);
value.nodeUID = node.getData('nodeUID');
}; };
/** /**
* List out Date node value in an array node. * List out Date node value in the same branch.
* *
* This will go through all array node and list from earlier date node to current date node. * This will go through all array node and list nodes that are sibling of the current node.
* *
* @method convertTreeDateValue * @method findAllDateSiblings
* @param {array} tree Tree node to convert * @param {Array} tree Tree items to convert
* @param {array} arrayDateNode * @param {Number} nodeUIDToFind node UID to find.
* @param {array} currentNode current node. * @return {Array|null} array of surrounding date avaiability values
*
* @return {array} arrayDateNode
*/ */
M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, currentNode) { M.availability_date.form.findAllDateSiblings = function(tree, nodeUIDToFind) {
var shouldSkip = false; var itemValue = null;
tree.forEach(function(node) { var siblingsFinderRecursive = function(itemsTree) {
if (shouldSkip) { var dateSiblings = [];
return; var nodeFound = false;
var index;
var childDates;
var currentOp = itemsTree.op !== undefined ? itemsTree.op : null;
if (itemsTree.c !== undefined) {
var children = itemsTree.c;
for (index = 0; index < children.length; index++) {
itemValue = children.at(index);
if (itemValue.type === undefined) {
childDates = siblingsFinderRecursive(itemValue);
if (childDates) {
return childDates;
} }
if (node.type === 'date') {
// We go through all tree node, if we meet the current node then return.
if (node.t === parseInt(currentNode.getData('time'), 10)
&& currentNode.one('select[name=direction]').get('value') == node.d) {
shouldSkip = true;
return;
} }
arrayDateNode.push(node); if (itemValue.type === 'date') {
} else if (node.type === undefined) { // We go through all tree node, if we meet the current node then we add all nodes in the current branch.
M.availability_date.form.convertTreeDateValue(node.c, arrayDateNode, currentNode); if (nodeUIDToFind === itemValue.nodeUID) {
nodeFound = true;
} else if (currentOp === '&') {
dateSiblings.push(itemValue);
} }
}); }
return arrayDateNode; }
if (nodeFound) {
return dateSiblings;
}
}
return null;
};
return siblingsFinderRecursive(tree);
}; };
/** /**
@ -181,37 +200,34 @@ M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, cu
* This will check current date node with all date node in tree node. * This will check current date node with all date node in tree node.
* *
* @method checkConditionDate * @method checkConditionDate
* @param {array} currentNode The curent node. * @param {Y.Node} currentNode The curent node.
* *
* @return {boolean} error Return true if the date is conflict. * @return {boolean} error Return true if the date is conflict.
*/ */
M.availability_date.form.checkConditionDate = function(currentNode) { M.availability_date.form.checkConditionDate = function(currentNode) {
var error = false; var error = false;
if (M.core_availability.form.rootList.getValue().op === '&') { var currentNodeUID = currentNode.getData('nodeUID');
var jsValue = M.core_availability.form.rootList.getValue().c;
var arrayDateNode = M.availability_date.form.convertTreeDateValue(jsValue, [], currentNode);
var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); var currentNodeDirection = currentNode.one('select[name=direction]').get('value');
var currentNodeTime = parseInt(currentNode.getData('time'), 10); var currentNodeTime = parseInt(currentNode.getData('time'), 10);
arrayDateNode.forEach(function(checkNode) { var dateSiblings = M.availability_date.form.findAllDateSiblings(
M.core_availability.form.rootList.getValue(),
currentNodeUID);
if (dateSiblings) {
dateSiblings.forEach(function(dateSibling) {
// Validate if the date is conflict. // Validate if the date is conflict.
if (checkNode.d === '<') { if (dateSibling.d === '<') {
if (currentNodeDirection === '>=' && currentNodeTime >= checkNode.t) { if (currentNodeDirection === '>=' && currentNodeTime >= dateSibling.t) {
error = true; error = true;
} }
} else { } else {
if (currentNodeDirection === '<' && currentNodeTime <= checkNode.t) { if (currentNodeDirection === '<' && currentNodeTime <= dateSibling.t) {
error = true; error = true;
} }
} }
return error; return error;
}); });
return error;
} else {
if (currentNode.one('div > .badge-warning')) {
currentNode.one('div > .badge-warning').remove();
} }
return error; return error;
}
}; };
M.availability_date.form.fillErrors = function(errors, node) { M.availability_date.form.fillErrors = function(errors, node) {

View File

@ -1 +1 @@
YUI.add("moodle-availability_date-form",function(o,e){M.availability_date=M.availability_date||{},M.availability_date.form=o.Object(M.core_availability.plugin),M.availability_date.form.initInner=function(e,a){this.html=e,this.defaultTime=a},M.availability_date.form.getNode=function(e){var t,i,a='<span class="col-form-label pr-3">'+M.util.get_string("direction_before","availability_date")+'</span> <span class="availability-group"><label><span class="accesshide">'+M.util.get_string("direction_label","availability_date")+' </span><select name="direction" class="custom-select"><option value="&gt;=">'+M.util.get_string("direction_from","availability_date")+'</option><option value="&lt;">'+M.util.get_string("direction_until","availability_date")+"</option></select></label></span> "+this.html,n=o.Node.create("<span>"+a+"</span>");return e.t!==undefined?(n.setData("time",e.t),n.all("select:not([name=direction])").each(function(e){e.set("disabled",!0)}),a=M.cfg.wwwroot+"/availability/condition/date/ajax.php?action=fromtime&time="+e.t,o.io(a,{on:{success:function(e,a){var t,i,l=o.JSON.parse(a.responseText);for(t in l)(i=n.one("select[name=x\\["+t+"\\]]")).set("value",""+l[t]),i.set("disabled",!1)},failure:function(){window.alert(M.util.get_string("ajaxerror","availability_date"))}}})):n.setData("time",this.defaultTime),e.d!==undefined&&n.one("select[name=direction]").set("value",e.d),M.availability_date.form.addedEvents||(M.availability_date.form.addedEvents=!0,(a=o.one(".availability-field")).delegate("change",function(){M.core_availability.form.update()},".availability_date select[name=direction]"),a.delegate("change",function(){M.availability_date.form.updateTime(this.ancestor("span.availability_date"))},".availability_date select:not([name=direction])")),n.one("a[href=#]")&&(M.form.dateselector.init_single_date_selector(n),t=n.one("select[name=x\\[year\\]]"),i=t.set,t.set=function(e,a){i.call(t,e,a),"selectedIndex"===e&&setTimeout(function(){M.availability_date.form.updateTime(n)},0)}),n},M.availability_date.form.updateTime=function(t){var e=M.cfg.wwwroot+"/availability/condition/date/ajax.php?action=totime&year="+t.one("select[name=x\\[year\\]]").get("value")+"&month="+t.one("select[name=x\\[month\\]]").get("value")+"&day="+t.one("select[name=x\\[day\\]]").get("value")+"&hour="+t.one("select[name=x\\[hour\\]]").get("value")+"&minute="+t.one("select[name=x\\[minute\\]]").get("value");o.io(e,{on:{success:function(e,a){t.setData("time",a.responseText),M.core_availability.form.update()},failure:function(){window.alert(M.util.get_string("ajaxerror","availability_date"))}}})},M.availability_date.form.fillValue=function(e,a){e.d=a.one("select[name=direction]").get("value"),e.t=parseInt(a.getData("time"),10)},M.availability_date.form.convertTreeDateValue=function(e,a,t){var i=!1;return e.forEach(function(e){i||("date"===e.type?e.t!==parseInt(t.getData("time"),10)||t.one("select[name=direction]").get("value")!=e.d?a.push(e):i=!0:e.type===undefined&&M.availability_date.form.convertTreeDateValue(e.c,a,t))}),a},M.availability_date.form.checkConditionDate=function(e){var a,t,i,l=!1;return"&"===M.core_availability.form.rootList.getValue().op?(a=M.core_availability.form.rootList.getValue().c,a=M.availability_date.form.convertTreeDateValue(a,[],e),t=e.one("select[name=direction]").get("value"),i=parseInt(e.getData("time"),10),a.forEach(function(e){return"<"===e.d?">="===t&&i>=e.t&&(l=!0):"<"===t&&i<=e.t&&(l=!0),l})):e.one("div > .badge-warning")&&e.one("div > .badge-warning").remove(),l},M.availability_date.form.fillErrors=function(e,a){M.availability_date.form.checkConditionDate(a)&&e.push("availability_date:error_dateconflict")}},"@VERSION@",{requires:["base","node","event","io","moodle-core_availability-form"]}); YUI.add("moodle-availability_date-form",function(o,e){M.availability_date=M.availability_date||{},M.availability_date.form=o.Object(M.core_availability.plugin),M.availability_date.form.initInner=function(e,a){this.html=e,this.defaultTime=a},M.availability_date.form.getNode=function(e){var t,i,a='<span class="col-form-label pr-3">'+M.util.get_string("direction_before","availability_date")+'</span> <span class="availability-group"><label><span class="accesshide">'+M.util.get_string("direction_label","availability_date")+' </span><select name="direction" class="custom-select"><option value="&gt;=">'+M.util.get_string("direction_from","availability_date")+'</option><option value="&lt;">'+M.util.get_string("direction_until","availability_date")+"</option></select></label></span> "+this.html,l=o.Node.create("<span>"+a+"</span>");return e.t!==undefined?(l.setData("time",e.t),l.all("select:not([name=direction])").each(function(e){e.set("disabled",!0)}),a=M.cfg.wwwroot+"/availability/condition/date/ajax.php?action=fromtime&time="+e.t,o.io(a,{on:{success:function(e,a){var t,i,n=o.JSON.parse(a.responseText);for(t in n)(i=l.one("select[name=x\\["+t+"\\]]")).set("value",""+n[t]),i.set("disabled",!1)},failure:function(){window.alert(M.util.get_string("ajaxerror","availability_date"))}}})):l.setData("time",this.defaultTime),e.nodeUID===undefined&&(a=new Date,e.nodeUID=a.getTime()),l.setData("nodeUID",e.nodeUID),e.d!==undefined&&l.one("select[name=direction]").set("value",e.d),M.availability_date.form.addedEvents||(M.availability_date.form.addedEvents=!0,(a=o.one(".availability-field")).delegate("change",function(){M.core_availability.form.update()},".availability_date select[name=direction]"),a.delegate("change",function(){M.availability_date.form.updateTime(this.ancestor("span.availability_date"))},".availability_date select:not([name=direction])")),l.one("a[href=#]")&&(M.form.dateselector.init_single_date_selector(l),t=l.one("select[name=x\\[year\\]]"),i=t.set,t.set=function(e,a){i.call(t,e,a),"selectedIndex"===e&&setTimeout(function(){M.availability_date.form.updateTime(l)},0)}),l},M.availability_date.form.updateTime=function(t){var e=M.cfg.wwwroot+"/availability/condition/date/ajax.php?action=totime&year="+t.one("select[name=x\\[year\\]]").get("value")+"&month="+t.one("select[name=x\\[month\\]]").get("value")+"&day="+t.one("select[name=x\\[day\\]]").get("value")+"&hour="+t.one("select[name=x\\[hour\\]]").get("value")+"&minute="+t.one("select[name=x\\[minute\\]]").get("value");o.io(e,{on:{success:function(e,a){t.setData("time",a.responseText),M.core_availability.form.update()},failure:function(){window.alert(M.util.get_string("ajaxerror","availability_date"))}}})},M.availability_date.form.fillValue=function(e,a){e.d=a.one("select[name=direction]").get("value"),e.t=parseInt(a.getData("time"),10),e.nodeUID=a.getData("nodeUID")},M.availability_date.form.findAllDateSiblings=function(e,d){var r,c=function(e){var a,t,i,n=[],l=!1,o=e.op!==undefined?e.op:null;if(e.c!==undefined){for(i=e.c,a=0;a<i.length;a++){if((r=i.at(a)).type===undefined&&(t=c(r)))return t;"date"===r.type&&(d===r.nodeUID?l=!0:"&"===o&&n.push(r))}if(l)return n}return null};return c(e)},M.availability_date.form.checkConditionDate=function(e){var a=!1,t=e.getData("nodeUID"),i=e.one("select[name=direction]").get("value"),n=parseInt(e.getData("time"),10),e=M.availability_date.form.findAllDateSiblings(M.core_availability.form.rootList.getValue(),t);return e&&e.forEach(function(e){return"<"===e.d?">="===i&&n>=e.t&&(a=!0):"<"===i&&n<=e.t&&(a=!0),a}),a},M.availability_date.form.fillErrors=function(e,a){M.availability_date.form.checkConditionDate(a)&&e.push("availability_date:error_dateconflict")}},"@VERSION@",{requires:["base","node","event","io","moodle-core_availability-form"]});

View File

@ -65,6 +65,11 @@ M.availability_date.form.getNode = function(json) {
// Set default time that corresponds to the HTML selectors. // Set default time that corresponds to the HTML selectors.
node.setData('time', this.defaultTime); node.setData('time', this.defaultTime);
} }
if (json.nodeUID === undefined) {
var miliTime = new Date();
json.nodeUID = miliTime.getTime();
}
node.setData('nodeUID', json.nodeUID);
if (json.d !== undefined) { if (json.d !== undefined) {
node.one('select[name=direction]').set('value', json.d); node.one('select[name=direction]').set('value', json.d);
} }
@ -114,7 +119,7 @@ M.availability_date.form.getNode = function(json) {
* gets an AJAX response. * gets an AJAX response.
* *
* @method updateTime * @method updateTime
* @param {Y.Node} component Node for plugin controls * @param {Y.Node} node Node for plugin controls
*/ */
M.availability_date.form.updateTime = function(node) { M.availability_date.form.updateTime = function(node) {
// After a change to the date/time we need to recompute the // After a change to the date/time we need to recompute the
@ -140,39 +145,53 @@ M.availability_date.form.updateTime = function(node) {
M.availability_date.form.fillValue = function(value, node) { M.availability_date.form.fillValue = function(value, node) {
value.d = node.one('select[name=direction]').get('value'); value.d = node.one('select[name=direction]').get('value');
value.t = parseInt(node.getData('time'), 10); value.t = parseInt(node.getData('time'), 10);
value.nodeUID = node.getData('nodeUID');
}; };
/** /**
* List out Date node value in an array node. * List out Date node value in the same branch.
* *
* This will go through all array node and list from earlier date node to current date node. * This will go through all array node and list nodes that are sibling of the current node.
* *
* @method convertTreeDateValue * @method findAllDateSiblings
* @param {array} tree Tree node to convert * @param {Array} tree Tree items to convert
* @param {array} arrayDateNode * @param {Number} nodeUIDToFind node UID to find.
* @param {array} currentNode current node. * @return {Array|null} array of surrounding date avaiability values
*
* @return {array} arrayDateNode
*/ */
M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, currentNode) { M.availability_date.form.findAllDateSiblings = function(tree, nodeUIDToFind) {
var shouldSkip = false; var itemValue = null;
tree.forEach(function(node) { var siblingsFinderRecursive = function(itemsTree) {
if (shouldSkip) { var dateSiblings = [];
return; var nodeFound = false;
var index;
var childDates;
var currentOp = itemsTree.op !== undefined ? itemsTree.op : null;
if (itemsTree.c !== undefined) {
var children = itemsTree.c;
for (index = 0; index < children.length; index++) {
itemValue = children.at(index);
if (itemValue.type === undefined) {
childDates = siblingsFinderRecursive(itemValue);
if (childDates) {
return childDates;
} }
if (node.type === 'date') {
// We go through all tree node, if we meet the current node then return.
if (node.t === parseInt(currentNode.getData('time'), 10)
&& currentNode.one('select[name=direction]').get('value') == node.d) {
shouldSkip = true;
return;
} }
arrayDateNode.push(node); if (itemValue.type === 'date') {
} else if (node.type === undefined) { // We go through all tree node, if we meet the current node then we add all nodes in the current branch.
M.availability_date.form.convertTreeDateValue(node.c, arrayDateNode, currentNode); if (nodeUIDToFind === itemValue.nodeUID) {
nodeFound = true;
} else if (currentOp === '&') {
dateSiblings.push(itemValue);
} }
}); }
return arrayDateNode; }
if (nodeFound) {
return dateSiblings;
}
}
return null;
};
return siblingsFinderRecursive(tree);
}; };
/** /**
@ -181,37 +200,34 @@ M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, cu
* This will check current date node with all date node in tree node. * This will check current date node with all date node in tree node.
* *
* @method checkConditionDate * @method checkConditionDate
* @param {array} currentNode The curent node. * @param {Y.Node} currentNode The curent node.
* *
* @return {boolean} error Return true if the date is conflict. * @return {boolean} error Return true if the date is conflict.
*/ */
M.availability_date.form.checkConditionDate = function(currentNode) { M.availability_date.form.checkConditionDate = function(currentNode) {
var error = false; var error = false;
if (M.core_availability.form.rootList.getValue().op === '&') { var currentNodeUID = currentNode.getData('nodeUID');
var jsValue = M.core_availability.form.rootList.getValue().c;
var arrayDateNode = M.availability_date.form.convertTreeDateValue(jsValue, [], currentNode);
var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); var currentNodeDirection = currentNode.one('select[name=direction]').get('value');
var currentNodeTime = parseInt(currentNode.getData('time'), 10); var currentNodeTime = parseInt(currentNode.getData('time'), 10);
arrayDateNode.forEach(function(checkNode) { var dateSiblings = M.availability_date.form.findAllDateSiblings(
M.core_availability.form.rootList.getValue(),
currentNodeUID);
if (dateSiblings) {
dateSiblings.forEach(function(dateSibling) {
// Validate if the date is conflict. // Validate if the date is conflict.
if (checkNode.d === '<') { if (dateSibling.d === '<') {
if (currentNodeDirection === '>=' && currentNodeTime >= checkNode.t) { if (currentNodeDirection === '>=' && currentNodeTime >= dateSibling.t) {
error = true; error = true;
} }
} else { } else {
if (currentNodeDirection === '<' && currentNodeTime <= checkNode.t) { if (currentNodeDirection === '<' && currentNodeTime <= dateSibling.t) {
error = true; error = true;
} }
} }
return error; return error;
}); });
return error;
} else {
if (currentNode.one('div > .badge-warning')) {
currentNode.one('div > .badge-warning').remove();
} }
return error; return error;
}
}; };
M.availability_date.form.fillErrors = function(errors, node) { M.availability_date.form.fillErrors = function(errors, node) {

View File

@ -63,6 +63,11 @@ M.availability_date.form.getNode = function(json) {
// Set default time that corresponds to the HTML selectors. // Set default time that corresponds to the HTML selectors.
node.setData('time', this.defaultTime); node.setData('time', this.defaultTime);
} }
if (json.nodeUID === undefined) {
var miliTime = new Date();
json.nodeUID = miliTime.getTime();
}
node.setData('nodeUID', json.nodeUID);
if (json.d !== undefined) { if (json.d !== undefined) {
node.one('select[name=direction]').set('value', json.d); node.one('select[name=direction]').set('value', json.d);
} }
@ -112,7 +117,7 @@ M.availability_date.form.getNode = function(json) {
* gets an AJAX response. * gets an AJAX response.
* *
* @method updateTime * @method updateTime
* @param {Y.Node} component Node for plugin controls * @param {Y.Node} node Node for plugin controls
*/ */
M.availability_date.form.updateTime = function(node) { M.availability_date.form.updateTime = function(node) {
// After a change to the date/time we need to recompute the // After a change to the date/time we need to recompute the
@ -138,39 +143,53 @@ M.availability_date.form.updateTime = function(node) {
M.availability_date.form.fillValue = function(value, node) { M.availability_date.form.fillValue = function(value, node) {
value.d = node.one('select[name=direction]').get('value'); value.d = node.one('select[name=direction]').get('value');
value.t = parseInt(node.getData('time'), 10); value.t = parseInt(node.getData('time'), 10);
value.nodeUID = node.getData('nodeUID');
}; };
/** /**
* List out Date node value in an array node. * List out Date node value in the same branch.
* *
* This will go through all array node and list from earlier date node to current date node. * This will go through all array node and list nodes that are sibling of the current node.
* *
* @method convertTreeDateValue * @method findAllDateSiblings
* @param {array} tree Tree node to convert * @param {Array} tree Tree items to convert
* @param {array} arrayDateNode * @param {Number} nodeUIDToFind node UID to find.
* @param {array} currentNode current node. * @return {Array|null} array of surrounding date avaiability values
*
* @return {array} arrayDateNode
*/ */
M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, currentNode) { M.availability_date.form.findAllDateSiblings = function(tree, nodeUIDToFind) {
var shouldSkip = false; var itemValue = null;
tree.forEach(function(node) { var siblingsFinderRecursive = function(itemsTree) {
if (shouldSkip) { var dateSiblings = [];
return; var nodeFound = false;
var index;
var childDates;
var currentOp = itemsTree.op !== undefined ? itemsTree.op : null;
if (itemsTree.c !== undefined) {
var children = itemsTree.c;
for (index = 0; index < children.length; index++) {
itemValue = children.at(index);
if (itemValue.type === undefined) {
childDates = siblingsFinderRecursive(itemValue);
if (childDates) {
return childDates;
} }
if (node.type === 'date') {
// We go through all tree node, if we meet the current node then return.
if (node.t === parseInt(currentNode.getData('time'), 10)
&& currentNode.one('select[name=direction]').get('value') == node.d) {
shouldSkip = true;
return;
} }
arrayDateNode.push(node); if (itemValue.type === 'date') {
} else if (node.type === undefined) { // We go through all tree node, if we meet the current node then we add all nodes in the current branch.
M.availability_date.form.convertTreeDateValue(node.c, arrayDateNode, currentNode); if (nodeUIDToFind === itemValue.nodeUID) {
nodeFound = true;
} else if (currentOp === '&') {
dateSiblings.push(itemValue);
} }
}); }
return arrayDateNode; }
if (nodeFound) {
return dateSiblings;
}
}
return null;
};
return siblingsFinderRecursive(tree);
}; };
/** /**
@ -179,37 +198,34 @@ M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, cu
* This will check current date node with all date node in tree node. * This will check current date node with all date node in tree node.
* *
* @method checkConditionDate * @method checkConditionDate
* @param {array} currentNode The curent node. * @param {Y.Node} currentNode The curent node.
* *
* @return {boolean} error Return true if the date is conflict. * @return {boolean} error Return true if the date is conflict.
*/ */
M.availability_date.form.checkConditionDate = function(currentNode) { M.availability_date.form.checkConditionDate = function(currentNode) {
var error = false; var error = false;
if (M.core_availability.form.rootList.getValue().op === '&') { var currentNodeUID = currentNode.getData('nodeUID');
var jsValue = M.core_availability.form.rootList.getValue().c;
var arrayDateNode = M.availability_date.form.convertTreeDateValue(jsValue, [], currentNode);
var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); var currentNodeDirection = currentNode.one('select[name=direction]').get('value');
var currentNodeTime = parseInt(currentNode.getData('time'), 10); var currentNodeTime = parseInt(currentNode.getData('time'), 10);
arrayDateNode.forEach(function(checkNode) { var dateSiblings = M.availability_date.form.findAllDateSiblings(
M.core_availability.form.rootList.getValue(),
currentNodeUID);
if (dateSiblings) {
dateSiblings.forEach(function(dateSibling) {
// Validate if the date is conflict. // Validate if the date is conflict.
if (checkNode.d === '<') { if (dateSibling.d === '<') {
if (currentNodeDirection === '>=' && currentNodeTime >= checkNode.t) { if (currentNodeDirection === '>=' && currentNodeTime >= dateSibling.t) {
error = true; error = true;
} }
} else { } else {
if (currentNodeDirection === '<' && currentNodeTime <= checkNode.t) { if (currentNodeDirection === '<' && currentNodeTime <= dateSibling.t) {
error = true; error = true;
} }
} }
return error; return error;
}); });
return error;
} else {
if (currentNode.one('div > .badge-warning')) {
currentNode.one('div > .badge-warning').remove();
} }
return error; return error;
}
}; };
M.availability_date.form.fillErrors = function(errors, node) { M.availability_date.form.fillErrors = function(errors, node) {

View File

@ -0,0 +1,67 @@
<?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/>.
require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
/**
* Availability related behat steps and selectors definitions.
*
* @package core_availability
* @category test
* @copyright 2023 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_availability extends behat_base {
/**
* Return the list of partial named selectors.
*
* @return array
*/
public static function get_partial_named_selectors(): array {
return [
new behat_component_named_selector(
'Activity availability', [
".//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]"
. "[descendant::*[contains(normalize-space(.), %locator%)]]//div[@data-region='availabilityinfo']",
]
),
new behat_component_named_selector(
'Section availability', [".//li[@id = %locator%]//div[@data-region='availabilityinfo']"],
),
new behat_component_named_selector(
'Set Of Restrictions', ["//div[h3[@data-restriction-order=%locator% and contains(text(), 'Set of')]]"],
),
];
}
/**
* Return the list of exact named selectors
*
* @return array
*/
public static function get_exact_named_selectors(): array {
return [
new behat_component_named_selector(
'Availability Button Area',
[
"//h3[@data-restriction-order=%locator%]/following-sibling::div[contains(@class,'availability-inner')]/"
. "div[contains(@class,'availability-button')]",
],
),
];
}
}

View File

@ -560,7 +560,7 @@ M.core_availability.List.prototype.renumber = function(parentNumber) {
} }
var heading = M.util.get_string('setheading', 'availability', headingParams); var heading = M.util.get_string('setheading', 'availability', headingParams);
this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').set('innerHTML', heading);
this.node.one('> h3').getDOMNode().dataset.restrictionOrder = parentNumber ? parentNumber : 'root';
// Do children. // Do children.
for (var i = 0; i < this.children.length; i++) { for (var i = 0; i < this.children.length; i++) {
var child = this.children[i]; var child = this.children[i];
@ -1008,6 +1008,7 @@ M.core_availability.Item.prototype.renumber = function(number) {
headingParams.number = number + ':'; headingParams.number = number + ':';
var heading = M.util.get_string('itemheading', 'availability', headingParams); var heading = M.util.get_string('itemheading', 'availability', headingParams);
this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').set('innerHTML', heading);
this.node.one('> h3').getDOMNode().dataset.restrictionOrder = number ? number : 'root';
}; };
/** /**

File diff suppressed because one or more lines are too long

View File

@ -560,7 +560,7 @@ M.core_availability.List.prototype.renumber = function(parentNumber) {
} }
var heading = M.util.get_string('setheading', 'availability', headingParams); var heading = M.util.get_string('setheading', 'availability', headingParams);
this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').set('innerHTML', heading);
this.node.one('> h3').getDOMNode().dataset.restrictionOrder = parentNumber ? parentNumber : 'root';
// Do children. // Do children.
for (var i = 0; i < this.children.length; i++) { for (var i = 0; i < this.children.length; i++) {
var child = this.children[i]; var child = this.children[i];
@ -1008,6 +1008,7 @@ M.core_availability.Item.prototype.renumber = function(number) {
headingParams.number = number + ':'; headingParams.number = number + ':';
var heading = M.util.get_string('itemheading', 'availability', headingParams); var heading = M.util.get_string('itemheading', 'availability', headingParams);
this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').set('innerHTML', heading);
this.node.one('> h3').getDOMNode().dataset.restrictionOrder = number ? number : 'root';
}; };
/** /**

View File

@ -558,7 +558,7 @@ M.core_availability.List.prototype.renumber = function(parentNumber) {
} }
var heading = M.util.get_string('setheading', 'availability', headingParams); var heading = M.util.get_string('setheading', 'availability', headingParams);
this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').set('innerHTML', heading);
this.node.one('> h3').getDOMNode().dataset.restrictionOrder = parentNumber ? parentNumber : 'root';
// Do children. // Do children.
for (var i = 0; i < this.children.length; i++) { for (var i = 0; i < this.children.length; i++) {
var child = this.children[i]; var child = this.children[i];
@ -1006,6 +1006,7 @@ M.core_availability.Item.prototype.renumber = function(number) {
headingParams.number = number + ':'; headingParams.number = number + ':';
var heading = M.util.get_string('itemheading', 'availability', headingParams); var heading = M.util.get_string('itemheading', 'availability', headingParams);
this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').set('innerHTML', heading);
this.node.one('> h3').getDOMNode().dataset.restrictionOrder = number ? number : 'root';
}; };
/** /**