diff --git a/amd/build/lifecycle.min.js b/amd/build/lifecycle.min.js index d5691ff..24c262f 100644 --- a/amd/build/lifecycle.min.js +++ b/amd/build/lifecycle.min.js @@ -1,3 +1,3 @@ -define("block_lifecycle/lifecycle",["exports","core/ajax","core/notification"],(function(_exports,_ajax,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);let defaultfreezedate="",originalfreezedatevalue="";function togglefreezebutton(checked){let readonlydateinput=document.getElementById("delayfreezedate");checked?(readonlydateinput.value="",readonlydateinput.disabled=!0):readonlydateinput.disabled=!1}function togglesettings(){let content=document.getElementById("automatic-read-only-settings");document.getElementById("override-freeze-date-button").classList.toggle("active"),content.style.maxHeight?content.style.maxHeight=null:content.style.maxHeight=content.scrollHeight+"px"}function initscheduledfreezedateblock(courseid){let scheduledfreezedatecontainer=document.getElementById("scheduled-freeze-date-container");originalfreezedatevalue=document.getElementById("delayfreezedate").value,scheduledfreezedatecontainer.style.display="none",_ajax.default.call([{methodname:"block_lifecycle_get_scheduled_freeze_date",args:{courseid:courseid}}])[0].done((function(response){document.getElementById("togglefreezebutton").checked?togglefreezebutton(!0):(document.getElementById("scheduled-freeze-date").innerHTML=response.scheduledfreezedate,scheduledfreezedatecontainer.style.display="block"),"true"===response.success&&(defaultfreezedate=response.defaultfreezedate)})).fail((function(err){window.console.log(err)}))}_exports.init=courseid=>{document.getElementById("lifecycle-settings-container")&&(initscheduledfreezedateblock(courseid),document.getElementById("togglefreezebutton").addEventListener("click",(event=>{togglefreezebutton(event.target.checked)})),document.getElementById("update_auto_freezing_preferences_button").addEventListener("click",(()=>{!function(courseid){let preferences={togglefreeze:document.getElementById("togglefreezebutton").checked,delayfreezedate:document.getElementById("delayfreezedate").value};(function(){let freezedateelement=document.getElementById("delayfreezedate");if(freezedateelement.value.length>0){if(""===defaultfreezedate)return _notification.default.alert("Error","Could not get the automatically suggested date, please try again later.","OK"),freezedateelement.value=originalfreezedatevalue,!1;{let defaultfreezedateobj=new Date(defaultfreezedate),freezedateobj=new Date(freezedateelement.value);if(freezedateobj{document.getElementById("lifecycle-settings-container")?(initscheduledfreezedateblock(courseid),document.getElementById("togglefreezebutton").addEventListener("click",(event=>{togglefreezebutton(event.target.checked)})),document.getElementById("update_auto_freezing_preferences_button").addEventListener("click",(()=>{!function(courseid){let preferences={togglefreeze:document.getElementById("togglefreezebutton").checked,delayfreezedate:document.getElementById("delayfreezedate").value};(function(){let freezedateelement=document.getElementById("delayfreezedate");if(freezedateelement.value.length>0){if(""===defaultfreezedate)return _notification.default.alert("Error","Could not get the automatically suggested date, please try again later.","OK"),freezedateelement.value=originalfreezedatevalue,!1;{let defaultfreezedateobj=new Date(defaultfreezedate),freezedateobj=new Date(freezedateelement.value);if(freezedateobj{event.preventDefault();const requiredStrings=[{key:"confirmcontextunlock",component:"admin",param:{contextname:contextname}}];(0,_str.getStrings)(requiredStrings).then((_ref=>{let[unlockBody]=_ref;return _notification.default.confirm("Enable editing",unlockBody,"Confirm",null,(()=>{_ajax.default.call([{methodname:"block_lifecycle_unfreeze_course",args:{courseid:courseid}}])[0].done((function(response){response.success?window.location.reload():(_notification.default.addNotification({message:response.message||"An error occurred while enabling editing.",type:"error"}),window.scrollTo({top:0,behavior:"instant"}))})).fail((function(err){window.console.log(err)}))}))})).catch((error=>(window.console.log(error),error)))}))}(courseid)}})); //# sourceMappingURL=lifecycle.min.js.map \ No newline at end of file diff --git a/amd/build/lifecycle.min.js.map b/amd/build/lifecycle.min.js.map index 6790e31..7b91834 100644 --- a/amd/build/lifecycle.min.js.map +++ b/amd/build/lifecycle.min.js.map @@ -1 +1 @@ -{"version":3,"file":"lifecycle.min.js","sources":["../src/lifecycle.js"],"sourcesContent":["import Ajax from 'core/ajax';\nimport notification from 'core/notification';\n\n// Default auto suggested read-only date.\nlet defaultfreezedate = '';\n// The datepicker original value before user make any changes.\nlet originalfreezedatevalue = '';\n\nexport const init = (courseid) => {\n // The course is read-only. Do nothing.\n if (!document.getElementById('lifecycle-settings-container')) {\n return;\n }\n\n initscheduledfreezedateblock(courseid);\n // On click listener for \"Disable Automatic Read-Only\" toggle.\n document.getElementById('togglefreezebutton').addEventListener('click', (event) => {\n togglefreezebutton(event.target.checked);\n });\n\n // Save button.\n document.getElementById('update_auto_freezing_preferences_button').addEventListener('click', () => {\n updatepreferences(courseid);\n });\n\n // Drop down settings button.\n document.getElementById('override-freeze-date-button').addEventListener(\"click\", function(e) {\n e.preventDefault();\n togglesettings();\n });\n};\n\n/**\n * Disable read-only date input when \"Disable Automatic Read-Only button\" is on.\n *\n * @param {boolean} checked\n */\nfunction togglefreezebutton(checked) {\n let readonlydateinput = document.getElementById('delayfreezedate');\n if (checked) {\n readonlydateinput.value = '';\n readonlydateinput.disabled = true;\n } else {\n readonlydateinput.disabled = false;\n }\n}\n\n/**\n * Toggle the automatic read only settings container.\n */\nfunction togglesettings() {\n let content = document.getElementById('automatic-read-only-settings');\n document.getElementById('override-freeze-date-button').classList.toggle('active');\n if (content.style.maxHeight) {\n content.style.maxHeight = null;\n } else {\n content.style.maxHeight = content.scrollHeight + \"px\";\n }\n}\n\n/**\n * Validate the preferences.\n * @return {boolean}\n */\nfunction validate() {\n let freezedateelement = document.getElementById('delayfreezedate');\n\n if (freezedateelement.value.length > 0) {\n // The default suggested date is not initialized, so cannot continue the checking.\n if (defaultfreezedate === '') {\n notification.alert(\n 'Error',\n 'Could not get the automatically suggested date, please try again later.',\n 'OK'\n );\n freezedateelement.value = originalfreezedatevalue;\n return false;\n } else {\n let defaultfreezedateobj = new Date(defaultfreezedate);\n let freezedateobj = new Date(freezedateelement.value);\n\n // The override freeze date should not be saved when it is earlier than the default suggested date.\n if (freezedateobj < defaultfreezedateobj || freezedateobj < new Date()) {\n notification.alert(\n 'Invalid Selection',\n 'The date for a Read-Only override must be post the automatically suggested date (' +\n defaultfreezedateobj.toLocaleDateString() +\n '), earlier dates may not be used.',\n 'OK'\n );\n freezedateelement.value = originalfreezedatevalue;\n return false;\n }\n }\n }\n\n return true;\n}\n\n/**\n * Initialize the scheduled freeze date container.\n *\n * @param {int} courseid\n */\nfunction initscheduledfreezedateblock(courseid) {\n let scheduledfreezedatecontainer = document.getElementById('scheduled-freeze-date-container');\n originalfreezedatevalue = document.getElementById('delayfreezedate').value;\n\n // Hide scheduled read-only date text at the beginning.\n scheduledfreezedatecontainer.style.display = 'none';\n\n // Get scheduled read-only dates.\n Ajax.call([{\n methodname: 'block_lifecycle_get_scheduled_freeze_date',\n args: {\n 'courseid': courseid\n },\n }])[0].done(function(response) {\n // Show scheduled date.\n if (!document.getElementById('togglefreezebutton').checked) {\n document.getElementById('scheduled-freeze-date').innerHTML = response.scheduledfreezedate;\n scheduledfreezedatecontainer.style.display = 'block';\n } else {\n // Disable read-only date input depends on freeze button status.\n togglefreezebutton(true);\n }\n // Set the default suggested date.\n if (response.success === 'true') {\n defaultfreezedate = response.defaultfreezedate;\n }\n }).fail(function(err) {\n window.console.log(err);\n });\n}\n\n/**\n * Update the auto context freezing preferences.\n * @param {int} courseid\n */\nfunction updatepreferences(courseid) {\n let preferences = {\n togglefreeze: document.getElementById('togglefreezebutton').checked,\n delayfreezedate: document.getElementById('delayfreezedate').value\n };\n\n if (validate()) {\n Ajax.call([{\n methodname: 'block_lifecycle_update_auto_freezing_preferences',\n args: {\n 'courseid': courseid,\n 'preferences': JSON.stringify(preferences)\n },\n }])[0].done(function(response) {\n notification.addNotification({\n message: response.message,\n type: response.success ? 'success' : 'error'\n });\n initscheduledfreezedateblock(courseid);\n togglesettings();\n }).fail(function(err) {\n window.console.log(err);\n });\n }\n}\n"],"names":["defaultfreezedate","originalfreezedatevalue","togglefreezebutton","checked","readonlydateinput","document","getElementById","value","disabled","togglesettings","content","classList","toggle","style","maxHeight","scrollHeight","initscheduledfreezedateblock","courseid","scheduledfreezedatecontainer","display","call","methodname","args","done","response","innerHTML","scheduledfreezedate","success","fail","err","window","console","log","addEventListener","event","target","preferences","togglefreeze","delayfreezedate","freezedateelement","length","alert","defaultfreezedateobj","Date","freezedateobj","toLocaleDateString","validate","JSON","stringify","addNotification","message","type","updatepreferences","e","preventDefault"],"mappings":"kXAIIA,kBAAoB,GAEpBC,wBAA0B,YA+BrBC,mBAAmBC,aACpBC,kBAAoBC,SAASC,eAAe,mBAC5CH,SACAC,kBAAkBG,MAAQ,GAC1BH,kBAAkBI,UAAW,GAE7BJ,kBAAkBI,UAAW,WAO5BC,qBACDC,QAAUL,SAASC,eAAe,gCACtCD,SAASC,eAAe,+BAA+BK,UAAUC,OAAO,UACpEF,QAAQG,MAAMC,UACdJ,QAAQG,MAAMC,UAAY,KAE1BJ,QAAQG,MAAMC,UAAYJ,QAAQK,aAAe,cAgDhDC,6BAA6BC,cAC9BC,6BAA+Bb,SAASC,eAAe,mCAC3DL,wBAA0BI,SAASC,eAAe,mBAAmBC,MAGrEW,6BAA6BL,MAAMM,QAAU,qBAGxCC,KAAK,CAAC,CACPC,WAAY,4CACZC,KAAM,UACUL,aAEhB,GAAGM,MAAK,SAASC,UAEZnB,SAASC,eAAe,sBAAsBH,QAK/CD,oBAAmB,IAJnBG,SAASC,eAAe,yBAAyBmB,UAAYD,SAASE,oBACtER,6BAA6BL,MAAMM,QAAU,SAMxB,SAArBK,SAASG,UACT3B,kBAAoBwB,SAASxB,sBAElC4B,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,sBA3HNZ,WAEZZ,SAASC,eAAe,kCAI7BU,6BAA6BC,UAE7BZ,SAASC,eAAe,sBAAsB2B,iBAAiB,SAAUC,QACrEhC,mBAAmBgC,MAAMC,OAAOhC,YAIpCE,SAASC,eAAe,2CAA2C2B,iBAAiB,SAAS,eAsHtEhB,cACnBmB,YAAc,CACdC,aAAchC,SAASC,eAAe,sBAAsBH,QAC5DmC,gBAAiBjC,SAASC,eAAe,mBAAmBC,uBA7E5DgC,kBAAoBlC,SAASC,eAAe,sBAE5CiC,kBAAkBhC,MAAMiC,OAAS,EAAG,IAEV,KAAtBxC,+CACiByC,MACT,QACA,0EACA,MAERF,kBAAkBhC,MAAQN,yBACnB,EACJ,KACCyC,qBAAuB,IAAIC,KAAK3C,mBAChC4C,cAAgB,IAAID,KAAKJ,kBAAkBhC,UAG3CqC,cAAgBF,sBAAwBE,cAAgB,IAAID,kCAC/CF,MACT,oBACA,oFACAC,qBAAqBG,qBACrB,oCACA,MAEJN,kBAAkBhC,MAAQN,yBACnB,UAKZ,GAiDH6C,kBACK1B,KAAK,CAAC,CACPC,WAAY,mDACZC,KAAM,UACUL,qBACG8B,KAAKC,UAAUZ,iBAElC,GAAGb,MAAK,SAASC,gCACJyB,gBAAgB,CACzBC,QAAS1B,SAAS0B,QAClBC,KAAM3B,SAASG,QAAU,UAAY,UAEzCX,6BAA6BC,UAC7BR,oBACDmB,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,QA1IvBuB,CAAkBnC,aAItBZ,SAASC,eAAe,+BAA+B2B,iBAAiB,SAAS,SAASoB,GACtFA,EAAEC,iBACF7C"} \ No newline at end of file +{"version":3,"file":"lifecycle.min.js","sources":["../src/lifecycle.js"],"sourcesContent":["import Ajax from 'core/ajax';\nimport notification from 'core/notification';\nimport {getStrings} from 'core/str';\n\n// Default auto suggested read-only date.\nlet defaultfreezedate = '';\n// The datepicker original value before user make any changes.\nlet originalfreezedatevalue = '';\n\nexport const init = (courseid) => {\n // The course is read-only. Initialize the unfreeze button and return.\n if (!document.getElementById('lifecycle-settings-container')) {\n // Initialize the unfreeze button.\n initUnfreezeButton(courseid);\n return;\n }\n\n initscheduledfreezedateblock(courseid);\n // On click listener for \"Disable Automatic Read-Only\" toggle.\n document.getElementById('togglefreezebutton').addEventListener('click', (event) => {\n togglefreezebutton(event.target.checked);\n });\n\n // Save button.\n document.getElementById('update_auto_freezing_preferences_button').addEventListener('click', () => {\n updatepreferences(courseid);\n });\n\n // Drop down settings button.\n document.getElementById('override-freeze-date-button').addEventListener(\"click\", function(e) {\n e.preventDefault();\n togglesettings();\n });\n};\n\n/**\n * Disable read-only date input when \"Disable Automatic Read-Only button\" is on.\n *\n * @param {boolean} checked\n */\nfunction togglefreezebutton(checked) {\n let readonlydateinput = document.getElementById('delayfreezedate');\n if (checked) {\n readonlydateinput.value = '';\n readonlydateinput.disabled = true;\n } else {\n readonlydateinput.disabled = false;\n }\n}\n\n/**\n * Toggle the automatic read only settings container.\n */\nfunction togglesettings() {\n let content = document.getElementById('automatic-read-only-settings');\n document.getElementById('override-freeze-date-button').classList.toggle('active');\n if (content.style.maxHeight) {\n content.style.maxHeight = null;\n } else {\n content.style.maxHeight = content.scrollHeight + \"px\";\n }\n}\n\n/**\n * Validate the preferences.\n * @return {boolean}\n */\nfunction validate() {\n let freezedateelement = document.getElementById('delayfreezedate');\n\n if (freezedateelement.value.length > 0) {\n // The default suggested date is not initialized, so cannot continue the checking.\n if (defaultfreezedate === '') {\n notification.alert(\n 'Error',\n 'Could not get the automatically suggested date, please try again later.',\n 'OK'\n );\n freezedateelement.value = originalfreezedatevalue;\n return false;\n } else {\n let defaultfreezedateobj = new Date(defaultfreezedate);\n let freezedateobj = new Date(freezedateelement.value);\n\n // The override freeze date should not be saved when it is earlier than the default suggested date.\n if (freezedateobj < defaultfreezedateobj || freezedateobj < new Date()) {\n notification.alert(\n 'Invalid Selection',\n 'The date for a Read-Only override must be post the automatically suggested date (' +\n defaultfreezedateobj.toLocaleDateString() +\n '), earlier dates may not be used.',\n 'OK'\n );\n freezedateelement.value = originalfreezedatevalue;\n return false;\n }\n }\n }\n\n return true;\n}\n\n/**\n * Initialize the scheduled freeze date container.\n *\n * @param {int} courseid\n */\nfunction initscheduledfreezedateblock(courseid) {\n let scheduledfreezedatecontainer = document.getElementById('scheduled-freeze-date-container');\n originalfreezedatevalue = document.getElementById('delayfreezedate').value;\n\n // Hide scheduled read-only date text at the beginning.\n scheduledfreezedatecontainer.style.display = 'none';\n\n // Get scheduled read-only dates.\n Ajax.call([{\n methodname: 'block_lifecycle_get_scheduled_freeze_date',\n args: {\n 'courseid': courseid\n },\n }])[0].done(function(response) {\n // Show scheduled date.\n if (!document.getElementById('togglefreezebutton').checked) {\n document.getElementById('scheduled-freeze-date').innerHTML = response.scheduledfreezedate;\n scheduledfreezedatecontainer.style.display = 'block';\n } else {\n // Disable read-only date input depends on freeze button status.\n togglefreezebutton(true);\n }\n // Set the default suggested date.\n if (response.success === 'true') {\n defaultfreezedate = response.defaultfreezedate;\n }\n }).fail(function(err) {\n window.console.log(err);\n });\n}\n\n/**\n * Update the auto context freezing preferences.\n * @param {int} courseid\n */\nfunction updatepreferences(courseid) {\n let preferences = {\n togglefreeze: document.getElementById('togglefreezebutton').checked,\n delayfreezedate: document.getElementById('delayfreezedate').value\n };\n\n if (validate()) {\n Ajax.call([{\n methodname: 'block_lifecycle_update_auto_freezing_preferences',\n args: {\n 'courseid': courseid,\n 'preferences': JSON.stringify(preferences)\n },\n }])[0].done(function(response) {\n notification.addNotification({\n message: response.message,\n type: response.success ? 'success' : 'error'\n });\n initscheduledfreezedateblock(courseid);\n togglesettings();\n }).fail(function(err) {\n window.console.log(err);\n });\n }\n}\n\n/**\n * Initialize the unfreeze button.\n *\n * @param {int} courseid\n */\nfunction initUnfreezeButton(courseid) {\n // Get the unfreeze button.\n let unfreezeButton = document.getElementById('unfreeze-button');\n\n // The course is not frozen. Do nothing.\n if (!unfreezeButton) {\n return;\n }\n\n let contextname = unfreezeButton.getAttribute('data-contextname');\n\n unfreezeButton.addEventListener('click', event => {\n event.preventDefault();\n\n const requiredStrings = [\n {key: 'confirmcontextunlock', component: 'admin', param: {'contextname': contextname}},\n ];\n\n getStrings(requiredStrings).then(([unlockBody]) => {\n return notification.confirm('Enable editing', unlockBody, 'Confirm', null, () => {\n Ajax.call([{\n methodname: 'block_lifecycle_unfreeze_course',\n args: {\n 'courseid': courseid\n },\n }])[0].done(function(response) {\n if (response.success) {\n window.location.reload();\n } else {\n notification.addNotification({\n message: response.message || 'An error occurred while enabling editing.',\n type: 'error'\n });\n // Scroll to the top of the page to show the error message.\n window.scrollTo({top: 0, behavior: \"instant\"});\n }\n }).fail(function(err) {\n window.console.log(err);\n });\n });\n }).catch(\n (error) => {\n window.console.log(error);\n return error;\n }\n );\n });\n}\n"],"names":["defaultfreezedate","originalfreezedatevalue","togglefreezebutton","checked","readonlydateinput","document","getElementById","value","disabled","togglesettings","content","classList","toggle","style","maxHeight","scrollHeight","initscheduledfreezedateblock","courseid","scheduledfreezedatecontainer","display","call","methodname","args","done","response","innerHTML","scheduledfreezedate","success","fail","err","window","console","log","addEventListener","event","target","preferences","togglefreeze","delayfreezedate","freezedateelement","length","alert","defaultfreezedateobj","Date","freezedateobj","toLocaleDateString","validate","JSON","stringify","addNotification","message","type","updatepreferences","e","preventDefault","unfreezeButton","contextname","getAttribute","requiredStrings","key","component","param","then","_ref","unlockBody","notification","confirm","location","reload","scrollTo","top","behavior","catch","error","initUnfreezeButton"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/lifecycle.js b/amd/src/lifecycle.js index caff124..62c2f98 100644 --- a/amd/src/lifecycle.js +++ b/amd/src/lifecycle.js @@ -1,5 +1,6 @@ import Ajax from 'core/ajax'; import notification from 'core/notification'; +import {getStrings} from 'core/str'; // Default auto suggested read-only date. let defaultfreezedate = ''; @@ -7,8 +8,10 @@ let defaultfreezedate = ''; let originalfreezedatevalue = ''; export const init = (courseid) => { - // The course is read-only. Do nothing. + // The course is read-only. Initialize the unfreeze button and return. if (!document.getElementById('lifecycle-settings-container')) { + // Initialize the unfreeze button. + initUnfreezeButton(courseid); return; } @@ -162,3 +165,57 @@ function updatepreferences(courseid) { }); } } + +/** + * Initialize the unfreeze button. + * + * @param {int} courseid + */ +function initUnfreezeButton(courseid) { + // Get the unfreeze button. + let unfreezeButton = document.getElementById('unfreeze-button'); + + // The course is not frozen. Do nothing. + if (!unfreezeButton) { + return; + } + + let contextname = unfreezeButton.getAttribute('data-contextname'); + + unfreezeButton.addEventListener('click', event => { + event.preventDefault(); + + const requiredStrings = [ + {key: 'confirmcontextunlock', component: 'admin', param: {'contextname': contextname}}, + ]; + + getStrings(requiredStrings).then(([unlockBody]) => { + return notification.confirm('Enable editing', unlockBody, 'Confirm', null, () => { + Ajax.call([{ + methodname: 'block_lifecycle_unfreeze_course', + args: { + 'courseid': courseid + }, + }])[0].done(function(response) { + if (response.success) { + window.location.reload(); + } else { + notification.addNotification({ + message: response.message || 'An error occurred while enabling editing.', + type: 'error' + }); + // Scroll to the top of the page to show the error message. + window.scrollTo({top: 0, behavior: "instant"}); + } + }).fail(function(err) { + window.console.log(err); + }); + }); + }).catch( + (error) => { + window.console.log(error); + return error; + } + ); + }); +} diff --git a/block_lifecycle.php b/block_lifecycle.php index 719f2ae..c99a038 100644 --- a/block_lifecycle.php +++ b/block_lifecycle.php @@ -73,6 +73,11 @@ public function get_content() { $html .= $renderer->fetch_clc_content($courseid); if (manager::is_course_frozen($courseid)) { $html .= $renderer->fetch_course_read_only_notification(); + + // Check if user has the capability to unfreeze the course. + if (has_capability("block/lifecycle:unfreezecourse", $context)) { + $html .= $renderer->fetch_unfreeze_button_html($context); + } } $html .= $renderer->fetch_course_dates($courseid); } diff --git a/classes/external/unfreeze_course.php b/classes/external/unfreeze_course.php new file mode 100644 index 0000000..b519fca --- /dev/null +++ b/classes/external/unfreeze_course.php @@ -0,0 +1,86 @@ +. + +namespace block_lifecycle\external; + +use block_lifecycle\manager; +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_value; + +/** + * External API for unfreeze a course + * + * @package block_lifecycle + * @copyright 2024 onwards University College London {@link https://www.ucl.ac.uk/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Alex Yeung + */ +class unfreeze_course extends external_api { + /** + * Returns description of method parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters([ + 'courseid' => new external_value(PARAM_INT, 'Coruse ID', VALUE_REQUIRED), + ]); + } + + /** + * Returns description of method result value. + * + * @return \external_single_structure + */ + public static function execute_returns() { + return new external_single_structure([ + 'success' => new external_value(PARAM_BOOL, 'Result of request', VALUE_REQUIRED), + 'message' => new external_value(PARAM_TEXT, 'Error message', VALUE_OPTIONAL), + ]); + } + + /** + * Unfreeze a course + * + * @param int $courseid + * @return array + */ + public static function execute(int $courseid) { + try { + $params = self::validate_parameters( + self::execute_parameters(), + [ + 'courseid' => $courseid, + ] + ); + + // Unfreeze the course. + manager::unfreeze_course($params['courseid']); + + return [ + 'success' => true, + 'message' => 'Enabled course editing successfully.', + ]; + } catch (\Exception $e) { + return [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + } + } +} diff --git a/classes/manager.php b/classes/manager.php index 066d911..9274da7 100644 --- a/classes/manager.php +++ b/classes/manager.php @@ -346,6 +346,23 @@ public static function get_weeks_delay_in_seconds() { return $enddateextend; } + /** + * Unfreeze course context. + * + * @param int $courseid Course id. + * @return void + * @throws \coding_exception + * @throws \moodle_exception + */ + public static function unfreeze_course(int $courseid): void { + // Check user's permission. + if (!has_capability('block/lifecycle:unfreezecourse', context_course::instance($courseid))) { + throw new \moodle_exception('error:unfreeze_course', 'block_lifecycle'); + } + $context = context_course::instance($courseid); + $context->set_locked(false); + } + /** * Get the furthest date among LSA end date and course end date, plus weeks delay. * diff --git a/db/access.php b/db/access.php index b320df8..486d107 100644 --- a/db/access.php +++ b/db/access.php @@ -57,4 +57,12 @@ 'manager' => CAP_ALLOW, ], ], + 'block/lifecycle:unfreezecourse' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => [ + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ], + ], ]; diff --git a/db/services.php b/db/services.php index 033caa3..172e741 100644 --- a/db/services.php +++ b/db/services.php @@ -34,6 +34,7 @@ 'description' => 'Update auto context freezing preferences', 'ajax' => true, 'type' => 'write', + 'readonlysession' => true, 'loginrequired' => true, ], 'block_lifecycle_get_scheduled_freeze_date' => [ @@ -43,6 +44,15 @@ 'description' => 'Get scheduled freeze date', 'ajax' => true, 'type' => 'read', + 'readonlysession' => true, + 'loginrequired' => true, + ], + 'block_lifecycle_unfreeze_course' => [ + 'classname' => 'block_lifecycle\external\unfreeze_course', + 'description' => 'Unfreeze a course', + 'ajax' => true, + 'type' => 'write', + 'readonlysession' => true, 'loginrequired' => true, ], ]; diff --git a/lang/en/block_lifecycle.php b/lang/en/block_lifecycle.php index 1078b04..9b9f822 100644 --- a/lang/en/block_lifecycle.php +++ b/lang/en/block_lifecycle.php @@ -23,27 +23,30 @@ * @author Alex Yeung */ -$string['pluginname'] = 'Lifecycle'; $string['button:editsettings'] = 'Edit automatic Read-Only settings'; $string['button:toggleautoreadonly'] = 'Disable Automatic Read-Only'; -$string['error:dateformat'] = 'Date must be in format YYYY-MM-DD'; $string['error:cannotgetscheduledfreezedate'] = 'Could not get the automatically suggested date.'; -$string['error:updatepreferencessuccess'] = 'Auto read only settings updated successfully.'; +$string['error:dateformat'] = 'Date must be in format YYYY-MM-DD'; +$string['error:unfreeze_course'] = 'You do not have permission to enable editing.'; $string['error:updatepreferencesfailed'] = 'Failed to update read only settings.'; +$string['error:updatepreferencessuccess'] = 'Auto read only settings updated successfully.'; $string['generalsettings'] = 'General Settings'; -$string['help:togglefreezing'] = 'Disable Automatic Read-Only'; -$string['help:togglefreezing_help'] = 'Disable Automatic Read-Only.'; $string['help:delayfreezedate'] = 'override Read-Only date'; $string['help:delayfreezedate_help'] = 'The date for a Read-Only override must be post the automatically suggested date, earlier dates may not be used.'; +$string['help:togglefreezing'] = 'Disable Automatic Read-Only'; +$string['help:togglefreezing_help'] = 'Disable Automatic Read-Only.'; $string['label:readonlydate'] = 'This course will be made automatically Read Only on: '; $string['label:readonlydateinput'] = 'Overrides Read-Only date:'; +$string['label:unfreezebutton'] = 'Enable editing'; $string['lifecycle:addinstance'] = 'Add lifecycle block'; +$string['lifecycle:coursereadonly'] = 'This Course is Read Only'; $string['lifecycle:enddate'] = 'This course\'s end date: {$a}'; $string['lifecycle:myaddinstance'] = 'Add my lifecycle block'; $string['lifecycle:overridecontextfreeze'] = 'Override default course context freezing settings'; $string['lifecycle:startdate'] = 'This course\'s start date: {$a}'; -$string['lifecycle:coursereadonly'] = 'This Course is Read Only'; +$string['lifecycle:unfreezecourse'] = 'Unfreeze course'; $string['lifecycle:view'] = 'View lifecycle block'; +$string['pluginname'] = 'Lifecycle'; $string['privacy:metadata'] = 'The Lifecycle block does not store personal data'; $string['settings:academicyearstartdate'] = 'Academic year start date'; $string['settings:academicyearstartdate:desc'] = 'This field is used to calculate the current academic year period and in MM-DD format'; diff --git a/renderer.php b/renderer.php index 2be3701..0748482 100644 --- a/renderer.php +++ b/renderer.php @@ -15,6 +15,7 @@ // along with Moodle. If not, see . use block_lifecycle\manager; +use core\context\course; /** * Class block_lifecycle_renderer @@ -151,4 +152,18 @@ public function fetch_course_read_only_notification(): string { return $content; } + + /** + * Return the html for the unfreeze button. + * + * @param course $context + * @return string + * @throws coding_exception + */ + public function fetch_unfreeze_button_html(core\context\course $context): string { + return + '' . + '' . get_string('label:unfreezebutton', 'block_lifecycle') . + ''; + } } diff --git a/tests/behat/behat_lifecycle.php b/tests/behat/behat_lifecycle.php new file mode 100644 index 0000000..5686fef --- /dev/null +++ b/tests/behat/behat_lifecycle.php @@ -0,0 +1,91 @@ +. + +use Behat\Gherkin\Node\TableNode; +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +/** + * Defines the behat steps for the block_lifecycle plugin. + * + * @package block_lifecycle + * @copyright 2024 onwards University College London {@link https://www.ucl.ac.uk/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Alex Yeung + */ +class behat_lifecycle extends behat_base { + /** + * Create custom field. + * + * @param TableNode $table + * @throws \dml_exception + * + * @Given /^the following custom field exists for lifecycle block:$/ + */ + public function the_following_custom_field_exists_for_lifecycle_block(TableNode $table): void { + global $DB; + + $data = $table->getRowsHash(); + + // Create a new custom field category if it doesn't exist. + $category = $DB->get_record( + 'customfield_category', + ['name' => $data['category'], + 'component' => 'core_course', + 'area' => 'course']); + + if (!$category) { + $category = (object)[ + 'name' => $data['category'], + 'component' => 'core_course', + 'area' => 'course', + 'sortorder' => 1, + 'timecreated' => time(), + 'timemodified' => time(), + ]; + $category->id = $DB->insert_record( + 'customfield_category', + $category + ); + } + + // Check if the field already exists. + $fieldexists = $DB->record_exists('customfield_field', ['shortname' => $data['shortname'], 'categoryid' => $category->id]); + + // Create the custom field if not exists. + if (!$fieldexists) { + $field = (object)[ + 'shortname' => $data['shortname'], + 'name' => $data['name'], + 'type' => $data['type'], + 'categoryid' => $category->id, + 'sortorder' => 0, + 'configdata' => json_encode([ + "required" => 0, + "uniquevalues" => 0, + "maxlength" => 4, + "defaultvalue" => "", + "ispassword" => 0, + "displaysize" => 4, + "locked" => 1, + "visibility" => 0, + ]), + 'timecreated' => time(), + 'timemodified' => time(), + ]; + $DB->insert_record('customfield_field', $field); + } + } +} diff --git a/tests/behat/unfreeze_course.feature b/tests/behat/unfreeze_course.feature new file mode 100644 index 0000000..ed86e5e --- /dev/null +++ b/tests/behat/unfreeze_course.feature @@ -0,0 +1,34 @@ +@block @block_lifecycle + +Feature: Unfreeze a frozen course + As a teacher with the appropriate permission + I can click on the "Enable editing" button in the lifecycle block to unfreeze a frozen course + + Background: + Given the following "users" exist: + | username | firstname | lastname | idnumber | email | + | teacher1 | Teacher1 | Test | tea1 | teacher1@example.com | + And the following custom field exists for lifecycle block: + | category | CLC | + | shortname | course_year | + | name | Course Year | + | type | text | + And the following "courses" exist: + | fullname | shortname | format | customfield_course_year | startdate | enddate | + | Course 1 | C1 | topics | ##now##%Y## | ## 2 days ago ## | ## yesterday ## | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | lifecycle | Course | C1 | course-view-* | side-pre | + And the "C1" "Course" is context frozen + + @javascript + Scenario: Unfreeze a frozen course + Given I am on the "C1" course page logged in as teacher1 + And edit mode should not be available on the current page + And I should see "Enable editing" in the "Lifecycle" "block" + And I click on "Enable editing" "text" + And I press "Confirm" + Then edit mode should be available on the current page diff --git a/tests/freezecontext_test.php b/tests/freezecontext_test.php index b015e80..29382a6 100644 --- a/tests/freezecontext_test.php +++ b/tests/freezecontext_test.php @@ -27,7 +27,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Alex Yeung */ -class freezecontext_test extends \advanced_testcase { +final class freezecontext_test extends \advanced_testcase { protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); diff --git a/tests/manager_test.php b/tests/manager_test.php index 03c6aee..b44bc0c 100644 --- a/tests/manager_test.php +++ b/tests/manager_test.php @@ -18,9 +18,8 @@ use coding_exception; use context_course; - -/** @var int TIME_NOW - set the current time to 2022-10-31 midnight*/ -const TIME_NOW = 1667174400; +use core_customfield\field_controller; +use moodle_exception; /** * Unit tests for block_lifecycle's manager class. @@ -30,10 +29,13 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Alex Yeung */ -class manager_test extends \advanced_testcase { +final class manager_test extends \advanced_testcase { + + /** @var field_controller field2 */ + private field_controller $field2; - /** @var \core_customfield\field_controller field2 */ - private $field2; + /** @var array years - Contains the year strings of past year, current year and future year */ + private array $years; protected function setUp(): void { global $DB; @@ -46,18 +48,31 @@ protected function setUp(): void { $this->field1 = $dg->create_custom_field(['categoryid' => $catid, 'type' => 'text', 'shortname' => 'course_year']); $this->field2 = $dg->create_custom_field(['categoryid' => $catid, 'type' => 'text', 'shortname' => 'new_field']); + // Put 4 years in associative array. + $this->years = [ + 'previous_year' => date('Y', strtotime('-2 years')), + 'last_year' => date('Y', strtotime('-1 years')), + 'current_year' => date('Y'), + 'next_year' => date('Y', strtotime('+1 years')), + ]; + // Create default courses. - $this->course1 = $dg->create_course(['customfield_course_year' => '2020']); - $this->course2 = $dg->create_course(['customfield_course_year' => '2021']); - $this->course3 = $dg->create_course(['customfield_course_year' => '2022']); - $this->course4 = $dg->create_course(['customfield_course_year' => '2023']); - $this->courseshouldbefrozen = $dg->create_course( - ['customfield_course_year' => '2021', 'startdate' => 1630450800, 'enddate' => 1656543600] - ); - $this->coursewithoutacademicyear = $dg->create_course(['startdate' => 1598914800, 'enddate' => 1625007600]); - $this->coursewithfutureenddate = $dg->create_course(['startdate' => 1598914800, 'enddate' => strtotime('+ 1 month')]); + $this->course1 = $dg->create_course(['customfield_course_year' => $this->years['previous_year']]); + $this->course2 = $dg->create_course(['customfield_course_year' => $this->years['last_year']]); + $this->course3 = $dg->create_course(['customfield_course_year' => $this->years['current_year']]); + $this->course4 = $dg->create_course(['customfield_course_year' => $this->years['next_year']]); + $this->courseshouldbefrozen = $dg->create_course([ + 'customfield_course_year' => $this->years['last_year'], + 'startdate' => strtotime('-10 months'), + 'enddate' => strtotime('-5 months'), + ]); + $this->coursewithoutacademicyear = $dg->create_course([ + 'startdate' => strtotime('-10 months'), + 'enddate' => strtotime('-5 months'), + ]); + $this->coursewithfutureenddate = $dg->create_course(['startdate' => time(), 'enddate' => strtotime('+ 1 month')]); $this->coursewithoutenddate = $dg->create_course( - ['customfield_course_year' => '2020', 'startdate' => 1598914800, 'enddate' => 0] + ['customfield_course_year' => $this->years['previous_year'], 'startdate' => strtotime('-2 years'), 'enddate' => 0] ); // Create roles. @@ -81,7 +96,7 @@ protected function setUp(): void { $this->preferences = new \stdClass(); $this->preferences->courseid = $this->courseshouldbefrozen->id; $this->preferences->freezeexcluded = 0; - $this->preferences->freezedate = strtotime('2022-10-31'); + $this->preferences->freezedate = strtotime(date('Y-m-d') . ' -1 day'); $this->preferences->timecreated = time(); $this->preferences->timemodified = time(); $this->preferencesrecordid = $DB->insert_record(manager::DEFAULT_TABLE, $this->preferences); @@ -98,9 +113,9 @@ public function test_get_potential_academic_years(): void { $years = manager::get_potential_academic_years(); $this->assertCount(4, $years); - // Set to use configured CLC field id. + // Test no potential academic years found. + // Set to use a dummy field, which is not the course academic year field. set_config('clcfield', $this->field2->get('id'), 'block_lifecycle'); - $years = manager::get_potential_academic_years(); $this->assertCount(0, $years); } @@ -274,17 +289,23 @@ public function test_get_course_clc_academic_year(): void { * @throws \ReflectionException */ public function test_get_course_lifecycle_info(): void { - // Test course academic year is 2020. + // Test course academic year is previous academic year. $result = manager::get_course_lifecycle_info($this->course1->id); - $this->assertEquals(['class' => '', 'text' => 'Moodle 2020/21'], $result); + $this->assertEquals(['class' => '', 'text' => $this->get_academic_year_string($this->years['previous_year'])], $result); // Test course academic year is current academic year. $result = manager::get_course_lifecycle_info($this->course3->id); - $this->assertEquals(['class' => 'current', 'text' => 'Moodle 2022/23'], $result); + $this->assertEquals( + ['class' => 'current', 'text' => $this->get_academic_year_string($this->years['current_year'])], + $result + ); // Test course academic year is future academic year. $result = manager::get_course_lifecycle_info($this->course4->id); - $this->assertEquals(['class' => 'future', 'text' => 'Moodle 2023/24'], $result); + $this->assertEquals( + ['class' => 'future', 'text' => $this->get_academic_year_string($this->years['next_year'])], + $result + ); } /** @@ -409,7 +430,7 @@ public function test_update_auto_freezing_preferences(): void { } /** - * Test Test is_course_frozen(). + * Test is_course_frozen(). * * @covers \block_lifecycle\manager::is_course_frozen() * @return void @@ -436,7 +457,7 @@ public function test_is_course_frozen(): void { public function test_get_auto_context_freezing_preferences(): void { $result = manager::get_auto_context_freezing_preferences($this->courseshouldbefrozen->id); $this->assertEquals('0', $result->freezeexcluded); - $this->assertEquals(strtotime('2022-10-31'), $result->freezedate); + $this->assertEquals(strtotime(date('Y-m-d') . ' -1 day'), $result->freezedate); } /** @@ -469,9 +490,9 @@ public function test_get_scheduled_freeze_date(): void { $this->preferences->freezedate = 0; $DB->update_record(manager::DEFAULT_TABLE, $this->preferences); set_config('weeks_delay', 1, 'block_lifecycle'); - set_config('late_summer_assessment_end_2021', date('Y-m-d', time()), 'block_lifecycle'); + set_config('late_summer_assessment_end_' . $this->years['last_year'], date('Y-m-d', time()), 'block_lifecycle'); $result = manager::get_scheduled_freeze_date($this->courseshouldbefrozen->id); - $datetime = new \DateTime(date('Y-m-d', TIME_NOW)); + $datetime = new \DateTime(date('Y-m-d')); $datetime->modify('+7 day'); $this->assertEquals($datetime->format('d/m/Y'), $result['scheduledfreezedate']); } @@ -554,15 +575,41 @@ public function test_get_furthest_date(): void { // Result equal to course end date plus weeks delay, 1 week in this case. $this->assertEquals(1646611200, $furthestdate); } -} -namespace block_lifecycle; -/** - * Overrides the PHP time() function to return a static time. - * - * @package block_lifecycle - * @return int - */ -function time(): int { - return TIME_NOW; + /** + * Test unfreeze_course(). + * + * @covers \block_lifecycle\manager::unfreeze_course() + * @return void + * @throws coding_exception + * @throws moodle_exception + */ + public function test_unfreeze_course(): void { + $this->setUser($this->user1); + try { + manager::unfreeze_course($this->courseshouldbefrozen->id); + } catch (moodle_exception $e) { + $this->assertEquals(get_string('error:unfreeze_course', 'block_lifecycle'), $e->getMessage()); + } + + // Freeze course. + $context = context_course::instance($this->courseshouldbefrozen->id); + $context->set_locked(true); + + // Test unlock course. + $this->getDataGenerator()->enrol_user($this->user1->id, $this->courseshouldbefrozen->id, $this->teacherroleid); + manager::unfreeze_course($this->courseshouldbefrozen->id); + $context = context_course::instance($this->courseshouldbefrozen->id); + $this->assertFalse($context->is_locked()); + } + + /** + * Get academic year string. + * + * @param int $year + * @return string + */ + private function get_academic_year_string(int $year): string { + return 'Moodle ' . $year . '/' . substr($year + 1, -2, 2); + } } diff --git a/tests/privacy_provider_test.php b/tests/privacy_provider_test.php index 136f00a..dd0a6f2 100644 --- a/tests/privacy_provider_test.php +++ b/tests/privacy_provider_test.php @@ -26,7 +26,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Alex Yeung */ -class privacy_provider_test extends \advanced_testcase { +final class privacy_provider_test extends \advanced_testcase { protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); diff --git a/version.php b/version.php index c792932..0081d74 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2022120800; +$plugin->version = 2022120803; $plugin->release = '0.1'; $plugin->maturity = MATURITY_ALPHA; $plugin->requires = 2020061512;