diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index 911b4e9..690aa6d 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -8,8 +8,8 @@ jobs: strategy: matrix: - php: ['8.2'] - moodle-branch: ['MOODLE_403_STABLE'] + php: ['8.3'] + moodle-branch: ['MOODLE_404_STABLE'] database: ['pgsql'] steps: @@ -125,8 +125,13 @@ jobs: strategy: fail-fast: false matrix: +<<<<<<< HEAD php: ['8.0', '8.1', '8.2', '8.3'] moodle-branch: ['MOODLE_401_STABLE', 'MOODLE_402_STABLE', 'MOODLE_403_STABLE', 'MOODLE_404_STABLE'] +======= + php: [ '8.0', '8.1', '8.2', '8.3' ] + moodle-branch: [ 'MOODLE_401_STABLE', 'MOODLE_402_STABLE', 'MOODLE_403_STABLE', 'MOODLE_404_STABLE' ] +>>>>>>> bf4f8e46d38657c979f74641afab4f65d030152c database: [ 'mariadb', 'pgsql' ] exclude: - php: '8.0' diff --git a/amd/build/coursefilter.min.js b/amd/build/coursefilter.min.js new file mode 100644 index 0000000..9122415 --- /dev/null +++ b/amd/build/coursefilter.min.js @@ -0,0 +1,14 @@ +define("block_townsquare/coursefilter",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(){checkboxes.forEach((function(checkbox){checkbox.addEventListener("change",(function(){const courseid=checkbox.id;document.querySelectorAll(".townsquare_letter.ts_timefilter_active.ts_letterfilter_active").forEach((function(letter){let letterCourseId=letter.querySelector(".townsquareletter_course").id;courseid===letterCourseId&&(checkbox.checked?letter.classList.add("ts_coursefilter_active"):letter.classList.remove("ts_coursefilter_active"))}))}))}))}; +/** + * Javascript for the course filter + * + * This file implements 1 functionality: + * - Checks the checkboxes of the course filter and hides content from courses if the checkbox is not checked. + * + * @module block_townsquare/coursefilter + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const checkboxes=document.querySelectorAll(".ts_course_checkbox")})); + +//# sourceMappingURL=coursefilter.min.js.map \ No newline at end of file diff --git a/amd/build/coursefilter.min.js.map b/amd/build/coursefilter.min.js.map new file mode 100644 index 0000000..de47173 --- /dev/null +++ b/amd/build/coursefilter.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"coursefilter.min.js","sources":["../src/coursefilter.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 .\n\n/**\n * Javascript for the course filter\n *\n * This file implements 1 functionality:\n * - Checks the checkboxes of the course filter and hides content from courses if the checkbox is not checked.\n *\n * @module block_townsquare/coursefilter\n * @copyright 2024 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Get the relevant checkboxes.\nconst checkboxes = document.querySelectorAll('.ts_course_checkbox');\n\n/**\n * Init function\n */\nexport function init() {\n checkboxes.forEach(function(checkbox) {\n checkbox.addEventListener('change', function() {\n // Get the courseid associated with the checkbox\n const courseid = checkbox.id;\n\n // Get all letters that are \"activated\".\n // Activated means that all filters accept the letter and want to show it.\n const letters = document.querySelectorAll('.townsquare_letter.ts_timefilter_active.ts_letterfilter_active');\n\n // Loop through each letter mark it as \"active\" or not based on checkbox state and the letter id.\n letters.forEach(function(letter) {\n let letterCourseId = letter.querySelector('.townsquareletter_course').id;\n\n if (courseid === letterCourseId) {\n if (checkbox.checked) {\n letter.classList.add('ts_coursefilter_active'); // Mark the letter as \"active\".\n } else {\n letter.classList.remove('ts_coursefilter_active'); // Mark the letter as \"not active\".\n }\n }\n });\n });\n });\n}\n"],"names":["checkboxes","forEach","checkbox","addEventListener","courseid","id","document","querySelectorAll","letter","letterCourseId","querySelector","checked","classList","add","remove"],"mappings":"6IAgCO,WACHA,WAAWC,SAAQ,SAASC,UACxBA,SAASC,iBAAiB,UAAU,WAEhC,MAAMC,SAAWF,SAASG,GAIVC,SAASC,iBAAiB,kEAGlCN,SAAQ,SAASO,QACrB,IAAIC,eAAiBD,OAAOE,cAAc,4BAA4BL,GAElED,WAAaK,iBACTP,SAASS,QACTH,OAAOI,UAAUC,IAAI,0BAErBL,OAAOI,UAAUE,OAAO;;;;;;;;;;;AAvBhD,MAAMd,WAAaM,SAASC,iBAAiB,sBA6B5C"} \ No newline at end of file diff --git a/amd/build/filtercontroller.min.js b/amd/build/filtercontroller.min.js new file mode 100644 index 0000000..4a17ab0 --- /dev/null +++ b/amd/build/filtercontroller.min.js @@ -0,0 +1,14 @@ +define("block_townsquare/filtercontroller",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(){letters.forEach((function(letter){letter.classList.add("ts_coursefilter_active"),letter.classList.add("ts_timefilter_active"),letter.classList.add("ts_letterfilter_active")})),letters.forEach((function(letter){new MutationObserver((function(mutations){mutations.forEach((function(mutation){if("class"===mutation.attributeName){let coursefilter=letter.classList.contains("ts_coursefilter_active"),timefilter=letter.classList.contains("ts_timefilter_active"),letterfilter=letter.classList.contains("ts_letterfilter_active");letter.style.display=coursefilter&&timefilter&&letterfilter?"block":"none"}}))})).observe(letter,{attributes:!0})}))}; +/** + * Javascript to show/hide letters based on all filters + * + * This file implements 1 functionality: + * - If the "save settings" button is pressed, store the settings in the database. + * + * @module block_townsquare/filtercontroller + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const letters=document.querySelectorAll(".townsquare_letter")})); + +//# sourceMappingURL=filtercontroller.min.js.map \ No newline at end of file diff --git a/amd/build/filtercontroller.min.js.map b/amd/build/filtercontroller.min.js.map new file mode 100644 index 0000000..ed17974 --- /dev/null +++ b/amd/build/filtercontroller.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"filtercontroller.min.js","sources":["../src/filtercontroller.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 .\n\n/**\n * Javascript to show/hide letters based on all filters\n *\n * This file implements 1 functionality:\n * - If the \"save settings\" button is pressed, store the settings in the database.\n *\n * @module block_townsquare/filtercontroller\n * @copyright 2024 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Get all letters from townsquare.\nconst letters = document.querySelectorAll('.townsquare_letter');\n\n/**\n * Init function\n */\nexport function init() {\n // First step: activate every letter by adding the filter classes.\n letters.forEach(function(letter) {\n letter.classList.add('ts_coursefilter_active');\n letter.classList.add('ts_timefilter_active');\n letter.classList.add('ts_letterfilter_active');\n });\n\n // Add a mutation listener to each letter.\n letters.forEach(function(letter) {\n const observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(mutation) {\n if (mutation.attributeName === 'class') {\n // If the class of the letter changes, check if the letter should be shown or hidden.\n let coursefilter = letter.classList.contains('ts_coursefilter_active');\n let timefilter = letter.classList.contains('ts_timefilter_active');\n let letterfilter = letter.classList.contains('ts_letterfilter_active');\n\n // If all filters are active, show the letter.\n if (coursefilter && timefilter && letterfilter) {\n letter.style.display = 'block';\n } else {\n letter.style.display = 'none';\n }\n }\n });\n });\n\n observer.observe(letter, {attributes: true});\n });\n}\n"],"names":["letters","forEach","letter","classList","add","MutationObserver","mutations","mutation","attributeName","coursefilter","contains","timefilter","letterfilter","style","display","observe","attributes","document","querySelectorAll"],"mappings":"iJAgCO,WAEHA,QAAQC,SAAQ,SAASC,QACrBA,OAAOC,UAAUC,IAAI,0BACrBF,OAAOC,UAAUC,IAAI,wBACrBF,OAAOC,UAAUC,IAAI,6BAIzBJ,QAAQC,SAAQ,SAASC,QACJ,IAAIG,kBAAiB,SAASC,WAC3CA,UAAUL,SAAQ,SAASM,UACvB,GAA+B,UAA3BA,SAASC,cAA2B,CAEpC,IAAIC,aAAeP,OAAOC,UAAUO,SAAS,0BACzCC,WAAaT,OAAOC,UAAUO,SAAS,wBACvCE,aAAeV,OAAOC,UAAUO,SAAS,0BAIzCR,OAAOW,MAAMC,QADbL,cAAgBE,YAAcC,aACP,QAEA,cAM9BG,QAAQb,OAAQ,CAACc,YAAY;;;;;;;;;;;AAjC9C,MAAMhB,QAAUiB,SAASC,iBAAiB,qBAmCzC"} \ No newline at end of file diff --git a/amd/build/letterfilter.min.js b/amd/build/letterfilter.min.js new file mode 100644 index 0000000..880a8cd --- /dev/null +++ b/amd/build/letterfilter.min.js @@ -0,0 +1,14 @@ +define("block_townsquare/letterfilter",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(){checkboxes.forEach((function(checkbox){checkbox.addEventListener("change",(function(){const lettername=checkbox.id;document.querySelectorAll(".townsquare_letter."+lettername+".ts_timefilter_active.ts_coursefilter_active").forEach((function(letter){checkbox.checked?letter.classList.add("ts_letterfilter_active"):letter.classList.remove("ts_letterfilter_active")}))}))}))}; +/** + * Javascript for the letter filter + * + * This file implements 1 functionality: + * - Checks the checkboxes of the letter filter and hides content from courses if the checkbox is not checked. + * + * @module block_townsquare/letterfilter + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const checkboxes=document.querySelectorAll(".ts_letter_checkbox")})); + +//# sourceMappingURL=letterfilter.min.js.map \ No newline at end of file diff --git a/amd/build/letterfilter.min.js.map b/amd/build/letterfilter.min.js.map new file mode 100644 index 0000000..02c4ab5 --- /dev/null +++ b/amd/build/letterfilter.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"letterfilter.min.js","sources":["../src/letterfilter.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 .\n\n/**\n * Javascript for the letter filter\n *\n * This file implements 1 functionality:\n * - Checks the checkboxes of the letter filter and hides content from courses if the checkbox is not checked.\n *\n * @module block_townsquare/letterfilter\n * @copyright 2024 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Get the relevant checkboxes.\nconst checkboxes = document.querySelectorAll('.ts_letter_checkbox');\n\n/**\n * Init function\n */\nexport function init() {\n checkboxes.forEach(function(checkbox) {\n checkbox.addEventListener('change', function() {\n // Get the letter name associated with the checkbox.\n const lettername = checkbox.id;\n\n // Get all letters that are \"activated\".\n // Activated means that all filters accept the letter and want to show it.\n const letters = document.querySelectorAll('.townsquare_letter.' + lettername +\n '.ts_timefilter_active.ts_coursefilter_active');\n\n // Loop through each letter and hide/show based on checkbox state.\n letters.forEach(function(letter) {\n if (checkbox.checked) {\n letter.classList.add('ts_letterfilter_active'); // Mark the letter as \"active\".\n } else {\n letter.classList.remove('ts_letterfilter_active'); // Mark the letter as \"not active\".\n }\n });\n });\n });\n}\n"],"names":["checkboxes","forEach","checkbox","addEventListener","lettername","id","document","querySelectorAll","letter","checked","classList","add","remove"],"mappings":"6IAgCO,WACHA,WAAWC,SAAQ,SAASC,UACxBA,SAASC,iBAAiB,UAAU,WAEhC,MAAMC,WAAaF,SAASG,GAIZC,SAASC,iBAAiB,sBAAwBH,WACxB,gDAGlCH,SAAQ,SAASO,QACjBN,SAASO,QACTD,OAAOE,UAAUC,IAAI,0BAErBH,OAAOE,UAAUE,OAAO;;;;;;;;;;;AArB5C,MAAMZ,WAAaM,SAASC,iBAAiB,sBA0B5C"} \ No newline at end of file diff --git a/amd/build/postletter.min.js.map b/amd/build/postletter.min.js.map index 46772fc..fde6c82 100644 --- a/amd/build/postletter.min.js.map +++ b/amd/build/postletter.min.js.map @@ -1 +1 @@ -{"version":3,"file":"postletter.min.js","sources":["../src/postletter.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 .\n\nimport {getString} from \"core/str\";\nimport {prefetchStrings} from 'core/prefetch';\n\n/**\n * Javascript for the post letter\n *\n * This file implements 2 functionalities:\n * - cuts posts that have many characters and shows a \"see more\" Button to see the whole text.\n * - Unnecessary

Tags from the Database are being replaced with line breaks to make the text more readable.\n *\n * @module block_townsquare/postletter\n * @copyright 2023 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst contentElements = document.getElementsByClassName('postletter_message');\nconst buttons = document.getElementsByClassName('townsquare_showmore');\nconst originalTexts = [];\n\nconst Selectors = {\n actions: {\n seemorebutton: '[data-action=\"block_townsquare/showmore_button\"]',\n },\n};\n\n/**\n * Init function\n *\n * The function can cut the text or extract paragraphs of a post.\n */\nexport function init() {\n contentElements.forEach(\n (element) => {\n // Replace all

within the text with simple line breaks..\n replaceParagraghTags(element);\n\n // Check if the text is too long.\n if (element.textContent.length >= 250) {\n // If the text is too long, cut it.\n originalTexts[element.id] = element.innerHTML;\n cutString(element);\n element.parentElement.insertAdjacentHTML('beforeend', '

');\n buttons[element.id].setAttribute('showmore', 'true');\n } else {\n // If the text is not too long, hide the show more button.\n buttons[element.id].style.display = \"none\";\n }\n }\n );\n\n // Get the strings for the show more/show less button.\n prefetchStrings('moodle', ['showmore', 'showless',]);\n\n // Add event listeners for the show more Button.\n addEventListener();\n}\n\n/**\n * Function to cut a String at a length of 250 characters.\n * The function does not cut within a word or after a space.\n * If the cutting point is within a word, the function searches for the next space and cuts there.\n * @param {object} element\n */\nfunction cutString(element) {\n let text = element.textContent;\n let index = 250;\n while (text.charAt(index) != \" \") {\n index++;\n }\n element.innerHTML = text.substring(0,index);\n}\n\n/**\n * Event listener for the show more/show less button.\n */\nconst addEventListener = () => {\n document.addEventListener('click', e => {\n if (e.target.closest(Selectors.actions.seemorebutton)) {\n // Get the id of the clicked element.\n let letterid = e.target.id;\n contentElements.forEach(\n (element) => {\n if (element.id == letterid) {\n if (buttons[letterid].getAttribute('showmore') == 'true') {\n element.innerHTML = originalTexts[letterid];\n changeButtonString(letterid, false);\n } else {\n cutString(element);\n changeButtonString(letterid, true);\n }\n }\n }\n );\n }\n });\n};\n\n/**\n * Changes the button strings.\n * @param {string} index Which button should be changed\n * @param {boolean} toshowmore a boolean that indicates if the button should show more or less\n */\nasync function changeButtonString(index, toshowmore) {\n if (toshowmore == true) {\n buttons[index].textContent = await getString('showmore', 'moodle');\n buttons[index].setAttribute('showmore', 'true');\n } else {\n buttons[index].textContent = await getString('showless', 'moodle');\n buttons[index].setAttribute('showmore', 'false');\n }\n}\n\n/**\n * Removes in a text all   and surrounding

tags excluding the first occurrence.\n *\n * Helper function to make post look better.\n * @param {object} element\n */\nasync function replaceParagraghTags(element) {\n // Identify and store the first

and

tags\n let message = element.innerHTML;\n const firstPTag = message.indexOf('

');\n const lastPTag = message.lastIndexOf('

');\n\n // Remove   and surrounding

tags excluding the first occurrence\n message = message.replace(/

 <\\/p>/g, '').replace(/ /g, '');\n\n // Replace

tags with
excluding the first occurrence\n message = message.substring(0, firstPTag + 3) +\n message.substring(firstPTag + 3, lastPTag).replace(/

/g, '
').replace(/<\\/p>/g, '') +\n message.substring(lastPTag);\n element.innerHTML = message;\n}"],"names":["contentElements","forEach","element","message","innerHTML","firstPTag","indexOf","lastPTag","lastIndexOf","replace","substring","replaceParagraghTags","textContent","length","originalTexts","id","cutString","parentElement","insertAdjacentHTML","buttons","setAttribute","style","display","addEventListener","document","getElementsByClassName","Selectors","seemorebutton","text","index","charAt","e","target","closest","letterid","getAttribute","changeButtonString","toshowmore"],"mappings":"gMA8CIA,gBAAgBC,SACXC,0BAsF2BA,aAE5BC,QAAUD,QAAQE,gBAChBC,UAAYF,QAAQG,QAAQ,OAC5BC,SAAWJ,QAAQK,YAAY,QAGrCL,QAAUA,QAAQM,QAAQ,kBAAmB,IAAIA,QAAQ,UAAW,IAGpEN,QAAUA,QAAQO,UAAU,EAAGL,UAAY,GACvCF,QAAQO,UAAUL,UAAY,EAAGE,UAAUE,QAAQ,OAAQ,QAAQA,QAAQ,SAAU,IACrFN,QAAQO,UAAUH,UACtBL,QAAQE,UAAYD,QAjGZQ,CAAqBT,SAGjBA,QAAQU,YAAYC,QAAU,KAE9BC,cAAcZ,QAAQa,IAAMb,QAAQE,UACpCY,UAAUd,SACVA,QAAQe,cAAcC,mBAAmB,YAAa,OACtDC,QAAQjB,QAAQa,IAAIK,aAAa,WAAY,SAG7CD,QAAQjB,QAAQa,IAAIM,MAAMC,QAAU,wCAMhC,SAAU,CAAC,WAAY,aAGvCC;;;;;;;;;;;;MAvCEvB,gBAAkBwB,SAASC,uBAAuB,sBAClDN,QAAUK,SAASC,uBAAuB,uBAC1CX,cAAgB,GAEhBY,kBACO,CACLC,cAAe,6DA0CdX,UAAUd,aACX0B,KAAO1B,QAAQU,YACfiB,MAAQ,SACiB,KAAtBD,KAAKE,OAAOD,QACfA,QAEJ3B,QAAQE,UAAYwB,KAAKlB,UAAU,EAAEmB,aAMnCN,iBAAmB,KACrBC,SAASD,iBAAiB,SAASQ,OAC3BA,EAAEC,OAAOC,QAAQP,kBAAkBC,eAAgB,KAE/CO,SAAWH,EAAEC,OAAOjB,GACxBf,gBAAgBC,SACXC,UACOA,QAAQa,IAAMmB,WACoC,QAA9Cf,QAAQe,UAAUC,aAAa,aAC/BjC,QAAQE,UAAYU,cAAcoB,UAClCE,mBAAmBF,UAAU,KAE7BlB,UAAUd,SACVkC,mBAAmBF,UAAU,4BAc1CE,mBAAmBP,MAAOQ,YACnB,GAAdA,YACAlB,QAAQU,OAAOjB,kBAAoB,kBAAU,WAAY,UACzDO,QAAQU,OAAOT,aAAa,WAAY,UAExCD,QAAQU,OAAOjB,kBAAoB,kBAAU,WAAY,UACzDO,QAAQU,OAAOT,aAAa,WAAY"} \ No newline at end of file +{"version":3,"file":"postletter.min.js","sources":["../src/postletter.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 .\n\nimport {getString} from \"core/str\";\nimport {prefetchStrings} from 'core/prefetch';\n\n/**\n * Javascript for the post letter\n *\n * This file implements 2 functionalities:\n * - cuts posts that have many characters and shows a \"see more\" Button to see the whole text.\n * - Unnecessary

Tags from the Database are being replaced with line breaks to make the text more readable.\n *\n * @module block_townsquare/postletter\n * @copyright 2023 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst contentElements = document.getElementsByClassName('postletter_message');\nconst buttons = document.getElementsByClassName('townsquare_showmore');\nconst originalTexts = [];\n\nconst Selectors = {\n actions: {\n seemorebutton: '[data-action=\"block_townsquare/showmore_button\"]',\n },\n};\n\n/**\n * Init function\n *\n * The function can cut the text or extract paragraphs of a post.\n */\nexport function init() {\n contentElements.forEach(\n (element) => {\n // Replace all

within the text with simple line breaks..\n replaceParagraghTags(element);\n\n // Check if the text is too long.\n if (element.textContent.length >= 250) {\n // If the text is too long, cut it.\n originalTexts[element.id] = element.innerHTML;\n cutString(element);\n element.parentElement.insertAdjacentHTML('beforeend', '

');\n buttons[element.id].setAttribute('showmore', 'true');\n } else {\n // If the text is not too long, hide the show more button.\n buttons[element.id].style.display = \"none\";\n }\n }\n );\n\n // Get the strings for the show more/show less button.\n prefetchStrings('moodle', ['showmore', 'showless',]);\n\n // Add event listeners for the show more Button.\n addEventListener();\n}\n\n/**\n * Function to cut a String at a length of 250 characters.\n * The function does not cut within a word or after a space.\n * If the cutting point is within a word, the function searches for the next space and cuts there.\n * @param {object} element\n */\nfunction cutString(element) {\n let text = element.textContent;\n let index = 250;\n while (text.charAt(index) != \" \") {\n index++;\n }\n element.innerHTML = text.substring(0,index);\n}\n\n/**\n * Event listener for the show more/show less button.\n */\nconst addEventListener = () => {\n document.addEventListener('click', e => {\n if (e.target.closest(Selectors.actions.seemorebutton)) {\n // Get the id of the clicked element.\n let letterid = e.target.id;\n contentElements.forEach(\n (element) => {\n if (element.id == letterid) {\n if (buttons[letterid].getAttribute('showmore') == 'true') {\n element.innerHTML = originalTexts[letterid];\n changeButtonString(letterid, false);\n } else {\n cutString(element);\n changeButtonString(letterid, true);\n }\n }\n }\n );\n }\n });\n};\n\n/**\n * Changes the button strings.\n * @param {string} index Which button should be changed\n * @param {boolean} toshowmore a boolean that indicates if the button should show more or less\n */\nasync function changeButtonString(index, toshowmore) {\n if (toshowmore == true) {\n buttons[index].textContent = await getString('showmore', 'moodle');\n buttons[index].setAttribute('showmore', 'true');\n } else {\n buttons[index].textContent = await getString('showless', 'moodle');\n buttons[index].setAttribute('showmore', 'false');\n }\n}\n\n/**\n * Removes in a text all   and surrounding

tags excluding the first occurrence.\n *\n * Helper function to make post look better.\n * @param {object} element\n */\nasync function replaceParagraghTags(element) {\n // Identify and store the first

and

tags\n let message = element.innerHTML;\n const firstPTag = message.indexOf('

');\n const lastPTag = message.lastIndexOf('

');\n\n // Remove   and surrounding

tags excluding the first occurrence\n message = message.replace(/

 <\\/p>/g, '').replace(/ /g, '');\n\n // Replace

tags with
excluding the first occurrence\n message = message.substring(0, firstPTag + 3) +\n message.substring(firstPTag + 3, lastPTag).replace(/

/g, '
').replace(/<\\/p>/g, '') +\n message.substring(lastPTag);\n element.innerHTML = message;\n}"],"names":["contentElements","forEach","element","async","message","innerHTML","firstPTag","indexOf","lastPTag","lastIndexOf","replace","substring","replaceParagraghTags","textContent","length","originalTexts","id","cutString","parentElement","insertAdjacentHTML","buttons","setAttribute","style","display","prefetchStrings","addEventListener","document","getElementsByClassName","Selectors","seemorebutton","text","index","charAt","e","target","closest","letterid","getAttribute","changeButtonString","toshowmore","getString"],"mappings":"qLA6CO,WACHA,gBAAgBC,SACXC,WAsFTC,eAAoCD,SAEhC,IAAIE,QAAUF,QAAQG,UACtB,MAAMC,UAAYF,QAAQG,QAAQ,OAC5BC,SAAWJ,QAAQK,YAAY,QAGrCL,QAAUA,QAAQM,QAAQ,kBAAmB,IAAIA,QAAQ,UAAW,IAGpEN,QAAUA,QAAQO,UAAU,EAAGL,UAAY,GACvCF,QAAQO,UAAUL,UAAY,EAAGE,UAAUE,QAAQ,OAAQ,QAAQA,QAAQ,SAAU,IACrFN,QAAQO,UAAUH,UACtBN,QAAQG,UAAYD,QAjGZQ,CAAqBV,SAGjBA,QAAQW,YAAYC,QAAU,KAE9BC,cAAcb,QAAQc,IAAMd,QAAQG,UACpCY,UAAUf,SACVA,QAAQgB,cAAcC,mBAAmB,YAAa,OACtDC,QAAQlB,QAAQc,IAAIK,aAAa,WAAY,SAG7CD,QAAQlB,QAAQc,IAAIM,MAAMC,QAAU,WAMhD,EAAAC,2BAAgB,SAAU,CAAC,WAAY,aAGvCC;;;;;;;;;;;;AAvCJ,MAAMzB,gBAAkB0B,SAASC,uBAAuB,sBAClDP,QAAUM,SAASC,uBAAuB,uBAC1CZ,cAAgB,GAEhBa,kBACO,CACLC,cAAe,oDA0CvB,SAASZ,UAAUf,SACf,IAAI4B,KAAO5B,QAAQW,YACfkB,MAAQ,IACZ,KAA6B,KAAtBD,KAAKE,OAAOD,QACfA,QAEJ7B,QAAQG,UAAYyB,KAAKnB,UAAU,EAAEoB,OAMzC,MAAMN,iBAAmBA,KACrBC,SAASD,iBAAiB,SAASQ,IAC/B,GAAIA,EAAEC,OAAOC,QAAQP,kBAAkBC,eAAgB,CAEnD,IAAIO,SAAWH,EAAEC,OAAOlB,GACxBhB,gBAAgBC,SACXC,UACOA,QAAQc,IAAMoB,WACoC,QAA9ChB,QAAQgB,UAAUC,aAAa,aAC/BnC,QAAQG,UAAYU,cAAcqB,UAClCE,mBAAmBF,UAAU,KAE7BnB,UAAUf,SACVoC,mBAAmBF,UAAU,WAMnD,EAQNjC,eAAemC,mBAAmBP,MAAOQ,YACnB,GAAdA,YACAnB,QAAQW,OAAOlB,kBAAoB,EAAA2B,gBAAU,WAAY,UACzDpB,QAAQW,OAAOV,aAAa,WAAY,UAExCD,QAAQW,OAAOlB,kBAAoB,EAAA2B,gBAAU,WAAY,UACzDpB,QAAQW,OAAOV,aAAa,WAAY,UAwB/C"} \ No newline at end of file diff --git a/amd/build/timefilter.min.js b/amd/build/timefilter.min.js new file mode 100644 index 0000000..8205054 --- /dev/null +++ b/amd/build/timefilter.min.js @@ -0,0 +1,14 @@ +define("block_townsquare/timefilter",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(){currenttime=(new Date).getTime()/1e3,alltimebutton.forEach((function(button){button.addEventListener("change",(function(){timestart=currenttime-convertidtotime(button.id),timeend=currenttime+convertidtotime(button.id),addstarttime=0,addendtime=0,futureradiobuttons.forEach((function(futureradiobutton){futureradiobutton.checked=!1,futureradiobutton.parentNode.classList.remove("active")})),pastradiobuttons.forEach((function(pastradiobutton){pastradiobutton.checked=!1,pastradiobutton.parentNode.classList.remove("active")})),executefilter(timestart,timeend,addstarttime,addendtime,button.checked)}))})),futureradiobuttons.forEach((function(button){button.addEventListener("change",(function(){alltimebutton.forEach((function(alltimebutton){alltimebutton.checked=!1,alltimebutton.parentNode.classList.remove("active")})),timestart=currenttime,timeend=currenttime+convertidtotime(button.id),addstarttime=0,addendtime=0,pastradiobuttons.forEach((function(pastradiobutton){pastradiobutton.parentNode.classList.contains("active")&&(addstarttime=currenttime-convertidtotime(pastradiobutton.id),addendtime=currenttime)})),executefilter(timestart,timeend,addstarttime,addendtime,button.checked)}))})),pastradiobuttons.forEach((function(button){button.addEventListener("change",(function(){alltimebutton.forEach((function(alltimebutton){alltimebutton.checked=!1,alltimebutton.parentNode.classList.remove("active")})),timestart=currenttime-convertidtotime(button.id),timeend=currenttime,addstarttime=0,addendtime=0,futureradiobuttons.forEach((function(futureradiobutton){futureradiobutton.parentNode.classList.contains("active")&&(addstarttime=currenttime,addendtime=currenttime+convertidtotime(futureradiobutton.id))})),executefilter(timestart,timeend,addstarttime,addendtime,button.checked)}))}))}; +/** + * Javascript for the time filter + * + * This file implements 1 functionality: + * - Checks, which of the radio buttons is pressed and filters the content based on the time. + * + * @module block_townsquare/timefilter + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const alltimebutton=document.querySelectorAll(".ts_all_time_button"),futureradiobuttons=document.querySelectorAll(".ts_future_time_button"),pastradiobuttons=document.querySelectorAll(".ts_past_time_button");let currenttime,timestart,timeend,addstarttime,addendtime;function executefilter(starttime,endtime,addstarttime,addendtime,buttonstate){document.querySelectorAll(".townsquare_letter.ts_coursefilter_active.ts_letterfilter_active").forEach((function(letter){let lettertime=letter.querySelector(".townsquareletter_date").id;buttonstate&&lettertime>=starttime&&lettertime<=endtime||lettertime>=addstarttime&&lettertime<=addendtime?letter.classList.add("ts_timefilter_active"):letter.classList.remove("ts_timefilter_active")}))}function convertidtotime(id){switch(id){case"ts_time_all":return 15778463;case"ts_time_next_twodays":case"ts_time_last_twodays":return 172800;case"ts_time_next_fivedays":case"ts_time_last_fivedays":return 432e3;case"ts_time_next_week":case"ts_time_last_week":return 604800;case"ts_time_next_month":case"ts_time_last_month":return 2592e3}}})); + +//# sourceMappingURL=timefilter.min.js.map \ No newline at end of file diff --git a/amd/build/timefilter.min.js.map b/amd/build/timefilter.min.js.map new file mode 100644 index 0000000..1763f9d --- /dev/null +++ b/amd/build/timefilter.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"timefilter.min.js","sources":["../src/timefilter.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 .\n\n/**\n * Javascript for the time filter\n *\n * This file implements 1 functionality:\n * - Checks, which of the radio buttons is pressed and filters the content based on the time.\n *\n * @module block_townsquare/timefilter\n * @copyright 2024 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Get the relevant radio buttons.\nconst alltimebutton = document.querySelectorAll('.ts_all_time_button');\nconst futureradiobuttons = document.querySelectorAll('.ts_future_time_button');\nconst pastradiobuttons = document.querySelectorAll('.ts_past_time_button');\n\n// Define to change the time span, an additional time span and the current time.\nlet currenttime;\nlet timestart;\nlet timeend;\nlet addstarttime;\nlet addendtime;\n\n/**\n * Init function\n */\nexport function init() {\n // Set the current time.\n currenttime = new Date().getTime() / 1000;\n\n // Add event listeners to the all kind of buttons.\n alltimeaddEventListener();\n futuretimeaddEventListener();\n pasttimeaddEventListener();\n}\n\n/**\n * Function to execute the filter\n * @param {int} starttime Start of time span for filtering of the current pressed button\n * @param {int} endtime End of time span for filtering of the current pressed\n * @param {int} addstarttime Start of time span for filtering of an additional radio button.\n * @param {int} addendtime End of time span for filtering of an additional radio button.\n * @param {boolean} buttonstate State of the radio button (true or false)\n */\nfunction executefilter(starttime, endtime, addstarttime, addendtime, buttonstate) {\n // Get all letters that are \"activated\".\n // Activated means that all filters accept the letter and want to show it.\n const letters = document.querySelectorAll('.townsquare_letter.ts_coursefilter_active.ts_letterfilter_active');\n\n // Loop through each letter and hide/show based on radiobutton state.\n letters.forEach(function(letter) {\n\n // Get the created time stamp of each letter.\n let lettertime = letter.querySelector('.townsquareletter_date').id;\n\n // If the radio button is checked and the letter is in the time span, activate it.\n if ((buttonstate && (lettertime >= starttime && lettertime <= endtime)) ||\n (lettertime >= addstarttime && lettertime <= addendtime)) {\n letter.classList.add('ts_timefilter_active'); // Mark the letter as \"active\".\n } else {\n letter.classList.remove('ts_timefilter_active'); // Mark the letter as \"not active\".\n }\n });\n}\n\n/**\n * Function to add event listeners to the all_time button.\n */\nfunction alltimeaddEventListener() {\n alltimebutton.forEach(function(button) {\n button.addEventListener('change', function() {\n // Set the time span to show all letters.\n timestart = currenttime - convertidtotime(button.id);\n timeend = currenttime + convertidtotime(button.id);\n addstarttime = 0;\n addendtime = 0;\n\n // Disable all other radio buttons that filter more specific times.\n futureradiobuttons.forEach(function(futureradiobutton) {\n futureradiobutton.checked = false;\n futureradiobutton.parentNode.classList.remove(\"active\");\n });\n pastradiobuttons.forEach(function(pastradiobutton) {\n pastradiobutton.checked = false;\n pastradiobutton.parentNode.classList.remove(\"active\");\n\n });\n\n // Execute the filter function.\n executefilter(timestart, timeend, addstarttime,addendtime, button.checked);\n });\n });\n}\n\n/**\n * Function to add event listeners to the future time radio buttons.\n */\nfunction futuretimeaddEventListener() {\n futureradiobuttons.forEach(function(button) {\n button.addEventListener('change', function() {\n // Disable the all_time button.\n alltimebutton.forEach(function(alltimebutton) {\n alltimebutton.checked = false;\n alltimebutton.parentNode.classList.remove('active');\n });\n\n // Set the time span based on the radiobutton id.\n timestart = currenttime;\n timeend = currenttime + convertidtotime(button.id);\n\n // Check if one past time button is checked. If yes, set the additional time span based on its id.\n addstarttime = 0;\n addendtime = 0;\n pastradiobuttons.forEach(function(pastradiobutton) {\n if (pastradiobutton.parentNode.classList.contains('active')) {\n addstarttime = currenttime - convertidtotime(pastradiobutton.id);\n addendtime = currenttime;\n }\n });\n\n // Execute the filter function.\n executefilter(timestart, timeend, addstarttime, addendtime, button.checked);\n });\n });\n}\n\n/**\n * Function to add event listeners to the past time radio buttons.\n */\nfunction pasttimeaddEventListener() {\n pastradiobuttons.forEach(function(button) {\n button.addEventListener('change', function() {\n // Disable the all_time button.\n alltimebutton.forEach(function(alltimebutton) {\n alltimebutton.checked = false;\n alltimebutton.parentNode.classList.remove('active');\n });\n\n // Set the time span based on the radiobutton id.\n timestart = currenttime - convertidtotime(button.id);\n timeend = currenttime;\n\n // Check if one future time button is checked. If yes, set the additional time span based on its id.\n addstarttime = 0;\n addendtime = 0;\n futureradiobuttons.forEach(function(futureradiobutton) {\n if (futureradiobutton.parentNode.classList.contains('active')) {\n addstarttime = currenttime;\n addendtime = currenttime + convertidtotime(futureradiobutton.id);\n }\n });\n\n // Execute the filter function.\n executefilter(timestart, timeend, addstarttime, addendtime, button.checked);\n });\n });\n}\n\n/**\n * Function to convert the radio button id to a useable time span.\n * @param {string} id The id of the radio button\n * @returns {number}\n */\nfunction convertidtotime(id) {\n switch(id) {\n case \"ts_time_all\":\n return 15778463;\n case \"ts_time_next_twodays\":\n case \"ts_time_last_twodays\":\n return 172800;\n case \"ts_time_next_fivedays\":\n case \"ts_time_last_fivedays\":\n return 432000;\n case \"ts_time_next_week\":\n case \"ts_time_last_week\":\n return 604800;\n case \"ts_time_next_month\":\n case \"ts_time_last_month\":\n return 2592000;\n }\n}\n"],"names":["currenttime","Date","getTime","alltimebutton","forEach","button","addEventListener","timestart","convertidtotime","id","timeend","addstarttime","addendtime","futureradiobuttons","futureradiobutton","checked","parentNode","classList","remove","pastradiobuttons","pastradiobutton","executefilter","contains","document","querySelectorAll","starttime","endtime","buttonstate","letter","lettertime","querySelector","add"],"mappings":"2IAyCO,WAEHA,aAAc,IAAIC,MAAOC,UAAY,IAyCrCC,cAAcC,SAAQ,SAASC,QAC3BA,OAAOC,iBAAiB,UAAU,WAE9BC,UAAYP,YAAcQ,gBAAgBH,OAAOI,IACjDC,QAAUV,YAAcQ,gBAAgBH,OAAOI,IAC/CE,aAAe,EACfC,WAAa,EAGbC,mBAAmBT,SAAQ,SAASU,mBAChCA,kBAAkBC,SAAU,EAC5BD,kBAAkBE,WAAWC,UAAUC,OAAO,aAElDC,iBAAiBf,SAAQ,SAASgB,iBAC9BA,gBAAgBL,SAAU,EAC1BK,gBAAgBJ,WAAWC,UAAUC,OAAO,aAKhDG,cAAcd,UAAWG,QAASC,aAAaC,WAAYP,OAAOU,eAS1EF,mBAAmBT,SAAQ,SAASC,QAChCA,OAAOC,iBAAiB,UAAU,WAE9BH,cAAcC,SAAQ,SAASD,eAC3BA,cAAcY,SAAU,EACxBZ,cAAca,WAAWC,UAAUC,OAAO,aAI9CX,UAAYP,YACZU,QAAUV,YAAcQ,gBAAgBH,OAAOI,IAG/CE,aAAe,EACfC,WAAa,EACbO,iBAAiBf,SAAQ,SAASgB,iBAC1BA,gBAAgBJ,WAAWC,UAAUK,SAAS,YAC9CX,aAAeX,YAAcQ,gBAAgBY,gBAAgBX,IAC7DG,WAAaZ,gBAKrBqB,cAAcd,UAAWG,QAASC,aAAcC,WAAYP,OAAOU,eAS3EI,iBAAiBf,SAAQ,SAASC,QAC9BA,OAAOC,iBAAiB,UAAU,WAE9BH,cAAcC,SAAQ,SAASD,eAC3BA,cAAcY,SAAU,EACxBZ,cAAca,WAAWC,UAAUC,OAAO,aAI9CX,UAAYP,YAAcQ,gBAAgBH,OAAOI,IACjDC,QAAUV,YAGVW,aAAe,EACfC,WAAa,EACbC,mBAAmBT,SAAQ,SAASU,mBAC5BA,kBAAkBE,WAAWC,UAAUK,SAAS,YAChDX,aAAeX,YACfY,WAAaZ,YAAcQ,gBAAgBM,kBAAkBL,QAKrEY,cAAcd,UAAWG,QAASC,aAAcC,WAAYP,OAAOU;;;;;;;;;;;AA7I/E,MAAMZ,cAAgBoB,SAASC,iBAAiB,uBAC1CX,mBAAqBU,SAASC,iBAAiB,0BAC/CL,iBAAmBI,SAASC,iBAAiB,wBAGnD,IAAIxB,YACAO,UACAG,QACAC,aACAC,WAuBJ,SAASS,cAAcI,UAAWC,QAASf,aAAcC,WAAYe,aAGjDJ,SAASC,iBAAiB,oEAGlCpB,SAAQ,SAASwB,QAGrB,IAAIC,WAAaD,OAAOE,cAAc,0BAA0BrB,GAG3DkB,aAAgBE,YAAcJ,WAAaI,YAAcH,SACzDG,YAAclB,cAAgBkB,YAAcjB,WAC7CgB,OAAOX,UAAUc,IAAI,wBAErBH,OAAOX,UAAUC,OAAO,2BAuGpC,SAASV,gBAAgBC,IACrB,OAAOA,IACH,IAAK,cACD,OAAO,SACX,IAAK,uBACL,IAAK,uBACD,OAAO,OACX,IAAK,wBACL,IAAK,wBACD,OAAO,MACX,IAAK,oBACL,IAAK,oBACD,OAAO,OACX,IAAK,qBACL,IAAK,qBACD,OAAO,QAElB"} \ No newline at end of file diff --git a/amd/build/usersettings_save.min.js b/amd/build/usersettings_save.min.js new file mode 100644 index 0000000..d1ebe0e --- /dev/null +++ b/amd/build/usersettings_save.min.js @@ -0,0 +1,13 @@ +define("block_townsquare/usersettings_save",["exports","core/ajax"],(function(_exports,_ajax){var obj; +/** + * Javascript to save the user settings in the database. + * + * This file implements 1 functionality: + * - If the "save settings" button is pressed, store the settings in the database. + * + * @module block_townsquare/usersettings_save + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(userid,settingsfromdb){settingsfromdb&&function(settingsfromdb){let futurebuttonid=converttimetoid(settingsfromdb.timefilterfuture,!0),pastbuttonid=converttimetoid(settingsfromdb.timefilterpast,!1);"ts_time_all"!==futurebuttonid?(futureradiobuttons.forEach((function(button){button.id===futurebuttonid&&(button.parentNode.classList.add("active"),button.checked=!0,button.dispatchEvent(new Event("change")),alltimebutton.forEach((function(alltimebutton){alltimebutton.checked=!1,alltimebutton.parentNode.classList.remove("active")})))})),pastradiobuttons.forEach((function(button){button.id===pastbuttonid&&(button.parentNode.classList.add("active"),button.checked=!0,button.dispatchEvent(new Event("change")),alltimebutton.forEach((function(alltimebutton){alltimebutton.checked=!1,alltimebutton.parentNode.classList.remove("active")})))}))):alltimebutton.forEach((function(button){button.parentNode.classList.add("active"),button.checked=!0,button.dispatchEvent(new Event("change"))}));checkboxes.forEach((function(checkbox){let basiclettercheck="basicletter"===checkbox.id&&""===settingsfromdb.basicletter,completionlettercheck="completionletter"===checkbox.id&&"0"===settingsfromdb.completionletter,postlettercheck="postletter"===checkbox.id&&"0"===settingsfromdb.postletter;(basiclettercheck||completionlettercheck||postlettercheck)&&checkbox.click()}))}(settingsfromdb);savebutton.addEventListener("click",(async function(){let timespans=function(){let settings={timepast:0,timefuture:0},settingsset=!1;if(alltimebutton.forEach((function(button){button.parentNode.classList.contains("active")&&(settings.timepast=convertidtotime(button.id),settings.timefuture=convertidtotime(button.id),settingsset=!0)})),settingsset)return settings;return futureradiobuttons.forEach((function(button){button.parentNode.classList.contains("active")&&(settings.timefuture=convertidtotime(button.id))})),pastradiobuttons.forEach((function(button){button.parentNode.classList.contains("active")&&(settings.timepast=convertidtotime(button.id))})),settings}(),letterfilter=function(){let settings={basicletter:0,completionletter:0,postletter:0};return checkboxes.forEach((function(checkbox){if(checkbox.checked)switch(checkbox.id){case"basicletter":settings.basicletter=1;break;case"completionletter":settings.completionletter=1;break;case"postletter":settings.postletter=1}})),settings}();await function(userid,timefilterpast,timefilterfuture,basicletter,completionletter,postletter){let result;const data={methodname:"block_townsquare_record_usersettings",args:{userid:userid,timefilterpast:timefilterpast,timefilterfuture:timefilterfuture,basicletter:basicletter,completionletter:completionletter,postletter:postletter}};return result=_ajax.default.call([data]),savebutton.classList.add("bg-success","text-white","ts_button_transition"),setTimeout((function(){savebutton.classList.remove("bg-success"),savebutton.classList.remove("text-white")}),1500),result}(userid,timespans.timepast,timespans.timefuture,letterfilter.basicletter,letterfilter.completionletter,letterfilter.postletter)}))},_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};const savebutton=document.getElementById("ts_usersettings_savebutton"),alltimebutton=document.querySelectorAll(".ts_all_time_button"),futureradiobuttons=document.querySelectorAll(".ts_future_time_button"),pastradiobuttons=document.querySelectorAll(".ts_past_time_button"),checkboxes=document.querySelectorAll(".ts_letter_checkbox");function convertidtotime(id){switch(id){case"ts_time_all":return 15778463;case"ts_time_next_twodays":case"ts_time_last_twodays":return 172800;case"ts_time_next_fivedays":case"ts_time_last_fivedays":return 432e3;case"ts_time_next_week":case"ts_time_last_week":return 604800;case"ts_time_next_month":case"ts_time_last_month":return 2592e3}}function converttimetoid(time,future){switch(time){case"15778463":return"ts_time_all";case"172800":return future?"ts_time_next_twodays":"ts_time_past_twodays";case"432000":return future?"ts_time_next_fivedays":"ts_time_last_fivedays";case"604800":return future?"ts_time_next_week":"ts_time_last_week";case"2592000":return future?"ts_time_next_month":"ts_time_last_month"}}})); + +//# sourceMappingURL=usersettings_save.min.js.map \ No newline at end of file diff --git a/amd/build/usersettings_save.min.js.map b/amd/build/usersettings_save.min.js.map new file mode 100644 index 0000000..8cc2c92 --- /dev/null +++ b/amd/build/usersettings_save.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"usersettings_save.min.js","sources":["../src/usersettings_save.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 .\n\n/**\n * Javascript to save the user settings in the database.\n *\n * This file implements 1 functionality:\n * - If the \"save settings\" button is pressed, store the settings in the database.\n *\n * @module block_townsquare/usersettings_save\n * @copyright 2024 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n// Get the save button for the user settings.\nconst savebutton = document.getElementById('ts_usersettings_savebutton');\n\n// Get the buttons from the time filter.\nconst alltimebutton = document.querySelectorAll('.ts_all_time_button');\nconst futureradiobuttons = document.querySelectorAll('.ts_future_time_button');\nconst pastradiobuttons = document.querySelectorAll('.ts_past_time_button');\n\n// Get the checkboxes from the letter filter.\nconst checkboxes = document.querySelectorAll('.ts_letter_checkbox');\n\n/**\n * Init function\n *\n * @param {number} userid The id of the current user.\n * @param {object} settingsfromdb The settings from the database, if there are any.\n */\nexport function init(userid, settingsfromdb) {\n // When the page is loaded, set the settings from the database.\n if (settingsfromdb) {\n executeusersettings(settingsfromdb);\n }\n\n // Add event listener to the save button.\n savebutton.addEventListener('click', async function() {\n\n // First step: collect the current settings.\n // Get the relevant time spans of the time filter and the setting of the letter filter checkboxes.\n let timespans = collecttimefiltersettings();\n let letterfilter = collectletterfiltersettings();\n\n // Second step: store the usersettings in the database.\n await saveusersettings(userid, timespans['timepast'], timespans['timefuture'], letterfilter['basicletter'],\n letterfilter['completionletter'], letterfilter['postletter']);\n });\n}\n\n/**\n * Function to save the user settings in the database.\n * @param {number} userid\n * @param {number} timefilterpast\n * @param {number} timefilterfuture\n * @param {number} basicletter\n * @param {number} completionletter\n * @param {number} postletter\n * @returns {Promise<*>}\n */\nfunction saveusersettings(userid, timefilterpast, timefilterfuture, basicletter, completionletter, postletter) {\n let result;\n\n const data = {\n methodname: 'block_townsquare_record_usersettings',\n args: {\n userid: userid,\n timefilterpast: timefilterpast,\n timefilterfuture: timefilterfuture,\n basicletter: basicletter,\n completionletter: completionletter,\n postletter: postletter,\n },\n };\n result = Ajax.call([data]);\n\n // Make the clicked button green by adding a class.\n savebutton.classList.add('bg-success', 'text-white', 'ts_button_transition');\n\n // Remove the classes after one second.\n setTimeout(function() {\n savebutton.classList.remove('bg-success');\n savebutton.classList.remove('text-white');\n }, 1500);\n return result;\n\n}\n\n/**\n * Function to execute existing user settings when loading the townsquare.\n * @param {Object} settingsfromdb\n */\nfunction executeusersettings(settingsfromdb) {\n\n // First step: set the time filter settings.\n // Change the time into the correct radio button id.\n let futurebuttonid = converttimetoid(settingsfromdb['timefilterfuture'], true);\n let pastbuttonid = converttimetoid(settingsfromdb['timefilterpast'], false);\n\n // If the time span is a combination of past and future, go through the two radio buttons and activate the filter.\n if (futurebuttonid !== \"ts_time_all\") {\n futureradiobuttons.forEach(function(button) {\n if (button.id === futurebuttonid) {\n button.parentNode.classList.add('active');\n button.checked = true;\n button.dispatchEvent(new Event('change'));\n alltimebutton.forEach(function(alltimebutton) {\n alltimebutton.checked = false;\n alltimebutton.parentNode.classList.remove('active');\n });\n }\n });\n pastradiobuttons.forEach(function(button) {\n if (button.id === pastbuttonid) {\n button.parentNode.classList.add('active');\n button.checked = true;\n button.dispatchEvent(new Event('change'));\n alltimebutton.forEach(function(alltimebutton) {\n alltimebutton.checked = false;\n alltimebutton.parentNode.classList.remove('active');\n });\n }\n });\n } else {\n // If the time span is set to all time, activate the all time button.\n alltimebutton.forEach(function(button) {\n button.parentNode.classList.add('active');\n button.checked = true;\n button.dispatchEvent(new Event('change'));\n });\n }\n\n // Second step: set the letter filter settings.\n // Per default all checkboxes are checked. If the setting is 0, uncheck the checkbox.\n checkboxes.forEach(function(checkbox) {\n let basiclettercheck = checkbox.id === 'basicletter' && settingsfromdb['basicletter'] === \"\";\n let completionlettercheck = checkbox.id === 'completionletter' && settingsfromdb['completionletter'] === \"0\";\n let postlettercheck = checkbox.id === 'postletter' && settingsfromdb['postletter'] === \"0\";\n\n if (basiclettercheck || completionlettercheck || postlettercheck) {\n checkbox.click();\n }\n });\n}\n\n/**\n * Function to collect the letter filter settings.\n * @returns {{basicletter: number, completionletter: number, postletter: number}}\n */\nfunction collectletterfiltersettings() {\n let settings = {'basicletter': 0, 'completionletter': 0, 'postletter': 0 };\n\n checkboxes.forEach(function(checkbox) {\n if (checkbox.checked) {\n switch(checkbox.id) {\n case \"basicletter\":\n settings['basicletter'] = 1;\n break;\n case \"completionletter\":\n settings['completionletter'] = 1;\n break;\n case \"postletter\":\n settings['postletter'] = 1;\n break;\n\n }\n }\n });\n // Calculate the setting number. It is a number between 0 and 7, and each letter represents a bit.\n return settings;\n}\n\n/**\n * Function to collect the time filter settings.\n * @returns {{timepast: number, timefuture: number}}\n */\nfunction collecttimefiltersettings() {\n let settings = { timepast: 0, timefuture: 0};\n let settingsset = false;\n\n // Get the relevant time spans of the time filter.\n // Check if the alltimebutton is set.\n alltimebutton.forEach(function(button) {\n if (button.parentNode.classList.contains('active')) {\n // Get the timespan.\n settings['timepast'] = convertidtotime(button.id);\n settings['timefuture'] = convertidtotime(button.id);\n settingsset = true;\n }\n });\n\n if (settingsset) {\n return settings;\n }\n\n // If the alltimebutton is not set, check which of the future/past buttons is set.\n futureradiobuttons.forEach(function(button) {\n if (button.parentNode.classList.contains('active')) {\n // Get the timespan.\n settings['timefuture'] = convertidtotime(button.id);\n }\n });\n\n pastradiobuttons.forEach(function(button) {\n if (button.parentNode.classList.contains('active')) {\n // Get the timespan.\n settings['timepast'] = convertidtotime(button.id);\n }\n });\n return settings;\n}\n\n\n/**\n * Function to convert the radio button id to a useable time span.\n * @param {string} id The id of the radio button\n * @returns {number}\n */\nfunction convertidtotime(id) {\n // TODO: Please use global functions if possible.\n switch(id) {\n case \"ts_time_all\":\n return 15778463;\n case \"ts_time_next_twodays\":\n case \"ts_time_last_twodays\":\n return 172800;\n case \"ts_time_next_fivedays\":\n case \"ts_time_last_fivedays\":\n return 432000;\n case \"ts_time_next_week\":\n case \"ts_time_last_week\":\n return 604800;\n case \"ts_time_next_month\":\n case \"ts_time_last_month\":\n return 2592000;\n }\n}\n\n/**\n * Function to convert the time span to a radio button id.\n * @param {string} time\n * @param {boolean} future\n * @returns {string}\n */\nfunction converttimetoid(time, future) {\n switch (time) {\n case \"15778463\":\n return \"ts_time_all\";\n case \"172800\":\n if (future) {\n return \"ts_time_next_twodays\";\n }\n return \"ts_time_past_twodays\";\n case \"432000\":\n if (future) {\n return \"ts_time_next_fivedays\";\n }\n return \"ts_time_last_fivedays\";\n case \"604800\":\n if (future) {\n return \"ts_time_next_week\";\n }\n return \"ts_time_last_week\";\n case \"2592000\":\n if (future) {\n return \"ts_time_next_month\";\n\n }\n return \"ts_time_last_month\";\n }\n}\n"],"names":["obj","userid","settingsfromdb","futurebuttonid","converttimetoid","pastbuttonid","futureradiobuttons","forEach","button","id","parentNode","classList","add","checked","dispatchEvent","Event","alltimebutton","remove","pastradiobuttons","checkboxes","checkbox","basiclettercheck","completionlettercheck","postlettercheck","click","executeusersettings","savebutton","addEventListener","async","timespans","settings","timepast","timefuture","settingsset","contains","convertidtotime","collecttimefiltersettings","letterfilter","basicletter","completionletter","postletter","collectletterfiltersettings","timefilterpast","timefilterfuture","result","data","methodname","args","Ajax","call","setTimeout","saveusersettings","_ajax","__esModule","default","document","getElementById","querySelectorAll","time","future"],"mappings":"8FA0B6B,IAAAA;;;;;;;;;;2EAmBtB,SAAcC,OAAQC,gBAErBA,gBA4DR,SAA6BA,gBAIzB,IAAIC,eAAiBC,gBAAgBF,eAAiC,kBAAG,GACrEG,aAAeD,gBAAgBF,eAA+B,gBAAG,GAG9C,gBAAnBC,gBACAG,mBAAmBC,SAAQ,SAASC,QAC5BA,OAAOC,KAAON,iBACdK,OAAOE,WAAWC,UAAUC,IAAI,UAChCJ,OAAOK,SAAU,EACjBL,OAAOM,cAAc,IAAIC,MAAM,WAC/BC,cAAcT,SAAQ,SAASS,eAC3BA,cAAcH,SAAU,EACxBG,cAAcN,WAAWC,UAAUM,OAAO,iBAItDC,iBAAiBX,SAAQ,SAASC,QAC1BA,OAAOC,KAAOJ,eACdG,OAAOE,WAAWC,UAAUC,IAAI,UAChCJ,OAAOK,SAAU,EACjBL,OAAOM,cAAc,IAAIC,MAAM,WAC/BC,cAAcT,SAAQ,SAASS,eAC3BA,cAAcH,SAAU,EACxBG,cAAcN,WAAWC,UAAUM,OAAO,kBAMtDD,cAAcT,SAAQ,SAASC,QAC3BA,OAAOE,WAAWC,UAAUC,IAAI,UAChCJ,OAAOK,SAAU,EACjBL,OAAOM,cAAc,IAAIC,MAAM,cAMvCI,WAAWZ,SAAQ,SAASa,UACxB,IAAIC,iBAAmC,gBAAhBD,SAASX,IAA0D,KAAlCP,eAA4B,YAChFoB,sBAAwC,qBAAhBF,SAASX,IAAoE,MAAvCP,eAAiC,iBAC/FqB,gBAAkC,eAAhBH,SAASX,IAAwD,MAAjCP,eAA2B,YAE7EmB,kBAAoBC,uBAAyBC,kBAC7CH,SAASI,WA3GbC,CAAoBvB,gBAIxBwB,WAAWC,iBAAiB,SAASC,iBAIjC,IAAIC,UAuIZ,WACI,IAAIC,SAAW,CAAEC,SAAU,EAAGC,WAAY,GACtCC,aAAc,EAalB,GATAjB,cAAcT,SAAQ,SAASC,QACxBA,OAAOE,WAAWC,UAAUuB,SAAS,YAErCJ,SAAmB,SAAIK,gBAAgB3B,OAAOC,IAC9CqB,SAAqB,WAAIK,gBAAgB3B,OAAOC,IAChDwB,aAAc,MAIjBA,YACA,OAAOH,SAiBX,OAbAxB,mBAAmBC,SAAQ,SAASC,QAC5BA,OAAOE,WAAWC,UAAUuB,SAAS,YAErCJ,SAAqB,WAAIK,gBAAgB3B,OAAOC,QAIxDS,iBAAiBX,SAAQ,SAASC,QAC1BA,OAAOE,WAAWC,UAAUuB,SAAS,YAErCJ,SAAmB,SAAIK,gBAAgB3B,OAAOC,QAG/CqB,SAxKaM,GACZC,aA2GZ,WACI,IAAIP,SAAW,CAACQ,YAAe,EAAGC,iBAAoB,EAAGC,WAAc,GAmBvE,OAjBArB,WAAWZ,SAAQ,SAASa,UACxB,GAAIA,SAASP,QACT,OAAOO,SAASX,IACZ,IAAK,cACDqB,SAAsB,YAAI,EAC1B,MACJ,IAAK,mBACDA,SAA2B,iBAAI,EAC/B,MACJ,IAAK,aACDA,SAAqB,WAAI,MAOlCA,SA/HgBW,SAkB3B,SAA0BxC,OAAQyC,eAAgBC,iBAAkBL,YAAaC,iBAAkBC,YAC/F,IAAII,OAEJ,MAAMC,KAAO,CACTC,WAAY,uCACZC,KAAM,CACF9C,OAAQA,OACRyC,eAAgBA,eAChBC,iBAAkBA,iBAClBL,YAAaA,YACbC,iBAAkBA,iBAClBC,WAAYA,aAapB,OAVAI,OAASI,cAAKC,KAAK,CAACJ,OAGpBnB,WAAWf,UAAUC,IAAI,aAAc,aAAc,wBAGrDsC,YAAW,WACPxB,WAAWf,UAAUM,OAAO,cAC5BS,WAAWf,UAAUM,OAAO,gBAC7B,MACI2B,OAvCGO,CAAiBlD,OAAQ4B,UAAoB,SAAGA,UAAsB,WAAGQ,aAA0B,YACrGA,aAA+B,iBAAGA,aAAyB,gBAnCvEe,OAA6BpD,IAA7BoD,QAA6BpD,IAAAqD,WAAArD,KAAAsD,QAAAtD,KAG7B,MAAM0B,WAAa6B,SAASC,eAAe,8BAGrCxC,cAAgBuC,SAASE,iBAAiB,uBAC1CnD,mBAAqBiD,SAASE,iBAAiB,0BAC/CvC,iBAAmBqC,SAASE,iBAAiB,wBAG7CtC,WAAaoC,SAASE,iBAAiB,uBAoM7C,SAAStB,gBAAgB1B,IAErB,OAAOA,IACH,IAAK,cACD,OAAO,SACX,IAAK,uBACL,IAAK,uBACD,OAAO,OACX,IAAK,wBACL,IAAK,wBACD,OAAO,MACX,IAAK,oBACL,IAAK,oBACD,OAAO,OACX,IAAK,qBACL,IAAK,qBACD,OAAO,QAUnB,SAASL,gBAAgBsD,KAAMC,QAC3B,OAAQD,MACJ,IAAK,WACD,MAAO,cACX,IAAK,SACD,OAAIC,OACO,uBAEJ,uBACX,IAAK,SACD,OAAIA,OACO,wBAEJ,wBACX,IAAK,SACD,OAAIA,OACO,oBAEJ,oBACX,IAAK,UACD,OAAIA,OACO,qBAGJ,sBAElB"} \ No newline at end of file diff --git a/amd/src/coursefilter.js b/amd/src/coursefilter.js new file mode 100644 index 0000000..04baad9 --- /dev/null +++ b/amd/src/coursefilter.js @@ -0,0 +1,57 @@ +// 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 . + +/** + * Javascript for the course filter + * + * This file implements 1 functionality: + * - Checks the checkboxes of the course filter and hides content from courses if the checkbox is not checked. + * + * @module block_townsquare/coursefilter + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Get the relevant checkboxes. +const checkboxes = document.querySelectorAll('.ts_course_checkbox'); + +/** + * Init function + */ +export function init() { + checkboxes.forEach(function(checkbox) { + checkbox.addEventListener('change', function() { + // Get the courseid associated with the checkbox + const courseid = checkbox.id; + + // Get all letters that are "activated". + // Activated means that all filters accept the letter and want to show it. + const letters = document.querySelectorAll('.townsquare_letter.ts_timefilter_active.ts_letterfilter_active'); + + // Loop through each letter mark it as "active" or not based on checkbox state and the letter id. + letters.forEach(function(letter) { + let letterCourseId = letter.querySelector('.townsquareletter_course').id; + + if (courseid === letterCourseId) { + if (checkbox.checked) { + letter.classList.add('ts_coursefilter_active'); // Mark the letter as "active". + } else { + letter.classList.remove('ts_coursefilter_active'); // Mark the letter as "not active". + } + } + }); + }); + }); +} diff --git a/amd/src/filtercontroller.js b/amd/src/filtercontroller.js new file mode 100644 index 0000000..11ccf50 --- /dev/null +++ b/amd/src/filtercontroller.js @@ -0,0 +1,63 @@ +// 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 . + +/** + * Javascript to show/hide letters based on all filters + * + * This file implements 1 functionality: + * - If the "save settings" button is pressed, store the settings in the database. + * + * @module block_townsquare/filtercontroller + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Get all letters from townsquare. +const letters = document.querySelectorAll('.townsquare_letter'); + +/** + * Init function + */ +export function init() { + // First step: activate every letter by adding the filter classes. + letters.forEach(function(letter) { + letter.classList.add('ts_coursefilter_active'); + letter.classList.add('ts_timefilter_active'); + letter.classList.add('ts_letterfilter_active'); + }); + + // Add a mutation listener to each letter. + letters.forEach(function(letter) { + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.attributeName === 'class') { + // If the class of the letter changes, check if the letter should be shown or hidden. + let coursefilter = letter.classList.contains('ts_coursefilter_active'); + let timefilter = letter.classList.contains('ts_timefilter_active'); + let letterfilter = letter.classList.contains('ts_letterfilter_active'); + + // If all filters are active, show the letter. + if (coursefilter && timefilter && letterfilter) { + letter.style.display = 'block'; + } else { + letter.style.display = 'none'; + } + } + }); + }); + + observer.observe(letter, {attributes: true}); + }); +} diff --git a/amd/src/letterfilter.js b/amd/src/letterfilter.js new file mode 100644 index 0000000..77c544c --- /dev/null +++ b/amd/src/letterfilter.js @@ -0,0 +1,54 @@ +// 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 . + +/** + * Javascript for the letter filter + * + * This file implements 1 functionality: + * - Checks the checkboxes of the letter filter and hides content from courses if the checkbox is not checked. + * + * @module block_townsquare/letterfilter + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Get the relevant checkboxes. +const checkboxes = document.querySelectorAll('.ts_letter_checkbox'); + +/** + * Init function + */ +export function init() { + checkboxes.forEach(function(checkbox) { + checkbox.addEventListener('change', function() { + // Get the letter name associated with the checkbox. + const lettername = checkbox.id; + + // Get all letters that are "activated". + // Activated means that all filters accept the letter and want to show it. + const letters = document.querySelectorAll('.townsquare_letter.' + lettername + + '.ts_timefilter_active.ts_coursefilter_active'); + + // Loop through each letter and hide/show based on checkbox state. + letters.forEach(function(letter) { + if (checkbox.checked) { + letter.classList.add('ts_letterfilter_active'); // Mark the letter as "active". + } else { + letter.classList.remove('ts_letterfilter_active'); // Mark the letter as "not active". + } + }); + }); + }); +} diff --git a/amd/src/timefilter.js b/amd/src/timefilter.js new file mode 100644 index 0000000..3a2b3c5 --- /dev/null +++ b/amd/src/timefilter.js @@ -0,0 +1,196 @@ +// 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 . + +/** + * Javascript for the time filter + * + * This file implements 1 functionality: + * - Checks, which of the radio buttons is pressed and filters the content based on the time. + * + * @module block_townsquare/timefilter + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Get the relevant radio buttons. +const alltimebutton = document.querySelectorAll('.ts_all_time_button'); +const futureradiobuttons = document.querySelectorAll('.ts_future_time_button'); +const pastradiobuttons = document.querySelectorAll('.ts_past_time_button'); + +// Define to change the time span, an additional time span and the current time. +let currenttime; +let timestart; +let timeend; +let addstarttime; +let addendtime; + +/** + * Init function + */ +export function init() { + // Set the current time. + currenttime = new Date().getTime() / 1000; + + // Add event listeners to the all kind of buttons. + alltimeaddEventListener(); + futuretimeaddEventListener(); + pasttimeaddEventListener(); +} + +/** + * Function to execute the filter + * @param {int} starttime Start of time span for filtering of the current pressed button + * @param {int} endtime End of time span for filtering of the current pressed + * @param {int} addstarttime Start of time span for filtering of an additional radio button. + * @param {int} addendtime End of time span for filtering of an additional radio button. + * @param {boolean} buttonstate State of the radio button (true or false) + */ +function executefilter(starttime, endtime, addstarttime, addendtime, buttonstate) { + // Get all letters that are "activated". + // Activated means that all filters accept the letter and want to show it. + const letters = document.querySelectorAll('.townsquare_letter.ts_coursefilter_active.ts_letterfilter_active'); + + // Loop through each letter and hide/show based on radiobutton state. + letters.forEach(function(letter) { + + // Get the created time stamp of each letter. + let lettertime = letter.querySelector('.townsquareletter_date').id; + + // If the radio button is checked and the letter is in the time span, activate it. + if ((buttonstate && (lettertime >= starttime && lettertime <= endtime)) || + (lettertime >= addstarttime && lettertime <= addendtime)) { + letter.classList.add('ts_timefilter_active'); // Mark the letter as "active". + } else { + letter.classList.remove('ts_timefilter_active'); // Mark the letter as "not active". + } + }); +} + +/** + * Function to add event listeners to the all_time button. + */ +function alltimeaddEventListener() { + alltimebutton.forEach(function(button) { + button.addEventListener('change', function() { + // Set the time span to show all letters. + timestart = currenttime - convertidtotime(button.id); + timeend = currenttime + convertidtotime(button.id); + addstarttime = 0; + addendtime = 0; + + // Disable all other radio buttons that filter more specific times. + futureradiobuttons.forEach(function(futureradiobutton) { + futureradiobutton.checked = false; + futureradiobutton.parentNode.classList.remove("active"); + }); + pastradiobuttons.forEach(function(pastradiobutton) { + pastradiobutton.checked = false; + pastradiobutton.parentNode.classList.remove("active"); + + }); + + // Execute the filter function. + executefilter(timestart, timeend, addstarttime,addendtime, button.checked); + }); + }); +} + +/** + * Function to add event listeners to the future time radio buttons. + */ +function futuretimeaddEventListener() { + futureradiobuttons.forEach(function(button) { + button.addEventListener('change', function() { + // Disable the all_time button. + alltimebutton.forEach(function(alltimebutton) { + alltimebutton.checked = false; + alltimebutton.parentNode.classList.remove('active'); + }); + + // Set the time span based on the radiobutton id. + timestart = currenttime; + timeend = currenttime + convertidtotime(button.id); + + // Check if one past time button is checked. If yes, set the additional time span based on its id. + addstarttime = 0; + addendtime = 0; + pastradiobuttons.forEach(function(pastradiobutton) { + if (pastradiobutton.parentNode.classList.contains('active')) { + addstarttime = currenttime - convertidtotime(pastradiobutton.id); + addendtime = currenttime; + } + }); + + // Execute the filter function. + executefilter(timestart, timeend, addstarttime, addendtime, button.checked); + }); + }); +} + +/** + * Function to add event listeners to the past time radio buttons. + */ +function pasttimeaddEventListener() { + pastradiobuttons.forEach(function(button) { + button.addEventListener('change', function() { + // Disable the all_time button. + alltimebutton.forEach(function(alltimebutton) { + alltimebutton.checked = false; + alltimebutton.parentNode.classList.remove('active'); + }); + + // Set the time span based on the radiobutton id. + timestart = currenttime - convertidtotime(button.id); + timeend = currenttime; + + // Check if one future time button is checked. If yes, set the additional time span based on its id. + addstarttime = 0; + addendtime = 0; + futureradiobuttons.forEach(function(futureradiobutton) { + if (futureradiobutton.parentNode.classList.contains('active')) { + addstarttime = currenttime; + addendtime = currenttime + convertidtotime(futureradiobutton.id); + } + }); + + // Execute the filter function. + executefilter(timestart, timeend, addstarttime, addendtime, button.checked); + }); + }); +} + +/** + * Function to convert the radio button id to a useable time span. + * @param {string} id The id of the radio button + * @returns {number} + */ +function convertidtotime(id) { + switch(id) { + case "ts_time_all": + return 15778463; + case "ts_time_next_twodays": + case "ts_time_last_twodays": + return 172800; + case "ts_time_next_fivedays": + case "ts_time_last_fivedays": + return 432000; + case "ts_time_next_week": + case "ts_time_last_week": + return 604800; + case "ts_time_next_month": + case "ts_time_last_month": + return 2592000; + } +} diff --git a/amd/src/usersettings_save.js b/amd/src/usersettings_save.js new file mode 100644 index 0000000..8a7b359 --- /dev/null +++ b/amd/src/usersettings_save.js @@ -0,0 +1,286 @@ +// 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 . + +/** + * Javascript to save the user settings in the database. + * + * This file implements 1 functionality: + * - If the "save settings" button is pressed, store the settings in the database. + * + * @module block_townsquare/usersettings_save + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from 'core/ajax'; + +// Get the save button for the user settings. +const savebutton = document.getElementById('ts_usersettings_savebutton'); + +// Get the buttons from the time filter. +const alltimebutton = document.querySelectorAll('.ts_all_time_button'); +const futureradiobuttons = document.querySelectorAll('.ts_future_time_button'); +const pastradiobuttons = document.querySelectorAll('.ts_past_time_button'); + +// Get the checkboxes from the letter filter. +const checkboxes = document.querySelectorAll('.ts_letter_checkbox'); + +/** + * Init function + * + * @param {number} userid The id of the current user. + * @param {object} settingsfromdb The settings from the database, if there are any. + */ +export function init(userid, settingsfromdb) { + // When the page is loaded, set the settings from the database. + if (settingsfromdb) { + executeusersettings(settingsfromdb); + } + + // Add event listener to the save button. + savebutton.addEventListener('click', async function() { + + // First step: collect the current settings. + // Get the relevant time spans of the time filter and the setting of the letter filter checkboxes. + let timespans = collecttimefiltersettings(); + let letterfilter = collectletterfiltersettings(); + + // Second step: store the usersettings in the database. + await saveusersettings(userid, timespans['timepast'], timespans['timefuture'], letterfilter['basicletter'], + letterfilter['completionletter'], letterfilter['postletter']); + }); +} + +/** + * Function to save the user settings in the database. + * @param {number} userid + * @param {number} timefilterpast + * @param {number} timefilterfuture + * @param {number} basicletter + * @param {number} completionletter + * @param {number} postletter + * @returns {Promise<*>} + */ +function saveusersettings(userid, timefilterpast, timefilterfuture, basicletter, completionletter, postletter) { + let result; + + const data = { + methodname: 'block_townsquare_record_usersettings', + args: { + userid: userid, + timefilterpast: timefilterpast, + timefilterfuture: timefilterfuture, + basicletter: basicletter, + completionletter: completionletter, + postletter: postletter, + }, + }; + result = Ajax.call([data]); + console.log('HI I AM HERE'); + // Make the clicked button green by adding a class. + savebutton.classList.add('bg-success', 'text-white', 'ts_button_transition'); + + // Remove the classes after one second. + setTimeout(function() { + savebutton.classList.remove('bg-success'); + savebutton.classList.remove('text-white'); + }, 1500); + return result; + +} + +/** + * Function to execute existing user settings when loading the townsquare. + * @param {Object} settingsfromdb + */ +function executeusersettings(settingsfromdb) { + + // First step: set the time filter settings. + // Change the time into the correct radio button id. + let futurebuttonid = converttimetoid(settingsfromdb['timefilterfuture'], true); + let pastbuttonid = converttimetoid(settingsfromdb['timefilterpast'], false); + + // If the time span is a combination of past and future, go through the two radio buttons and activate the filter. + if (futurebuttonid !== "ts_time_all") { + futureradiobuttons.forEach(function(button) { + if (button.id === futurebuttonid) { + button.parentNode.classList.add('active'); + button.checked = true; + button.dispatchEvent(new Event('change')); + alltimebutton.forEach(function(alltimebutton) { + alltimebutton.checked = false; + alltimebutton.parentNode.classList.remove('active'); + }); + } + }); + pastradiobuttons.forEach(function(button) { + if (button.id === pastbuttonid) { + button.parentNode.classList.add('active'); + button.checked = true; + button.dispatchEvent(new Event('change')); + alltimebutton.forEach(function(alltimebutton) { + alltimebutton.checked = false; + alltimebutton.parentNode.classList.remove('active'); + }); + } + }); + } else { + // If the time span is set to all time, activate the all time button. + alltimebutton.forEach(function(button) { + button.parentNode.classList.add('active'); + button.checked = true; + button.dispatchEvent(new Event('change')); + }); + } + + // Second step: set the letter filter settings. + // Per default all checkboxes are checked. If the setting is 0, uncheck the checkbox. + checkboxes.forEach(function(checkbox) { + let basiclettercheck = checkbox.id === 'basicletter' && settingsfromdb['basicletter'] === ""; + let completionlettercheck = checkbox.id === 'completionletter' && settingsfromdb['completionletter'] === "0"; + let postlettercheck = checkbox.id === 'postletter' && settingsfromdb['postletter'] === "0"; + + if (basiclettercheck || completionlettercheck || postlettercheck) { + checkbox.click(); + } + }); +} + +/** + * Function to collect the letter filter settings. + * @returns {{basicletter: number, completionletter: number, postletter: number}} + */ +function collectletterfiltersettings() { + let settings = {'basicletter': 0, 'completionletter': 0, 'postletter': 0 }; + + checkboxes.forEach(function(checkbox) { + if (checkbox.checked) { + switch(checkbox.id) { + case "basicletter": + settings['basicletter'] = 1; + break; + case "completionletter": + settings['completionletter'] = 1; + break; + case "postletter": + settings['postletter'] = 1; + break; + + } + } + }); + // Calculate the setting number. It is a number between 0 and 7, and each letter represents a bit. + return settings; +} + +/** + * Function to collect the time filter settings. + * @returns {{timepast: number, timefuture: number}} + */ +function collecttimefiltersettings() { + let settings = { timepast: 0, timefuture: 0}; + let settingsset = false; + + // Get the relevant time spans of the time filter. + // Check if the alltimebutton is set. + alltimebutton.forEach(function(button) { + if (button.parentNode.classList.contains('active')) { + // Get the timespan. + settings['timepast'] = convertidtotime(button.id); + settings['timefuture'] = convertidtotime(button.id); + settingsset = true; + } + }); + + if (settingsset) { + return settings; + } + + // If the alltimebutton is not set, check which of the future/past buttons is set. + futureradiobuttons.forEach(function(button) { + if (button.parentNode.classList.contains('active')) { + // Get the timespan. + settings['timefuture'] = convertidtotime(button.id); + } + }); + + pastradiobuttons.forEach(function(button) { + if (button.parentNode.classList.contains('active')) { + // Get the timespan. + settings['timepast'] = convertidtotime(button.id); + } + }); + return settings; +} + + +/** + * Function to convert the radio button id to a useable time span. + * @param {string} id The id of the radio button + * @returns {number} + */ +function convertidtotime(id) { + // TODO: Please use global functions if possible. + switch(id) { + case "ts_time_all": + return 15778463; + case "ts_time_next_twodays": + case "ts_time_last_twodays": + return 172800; + case "ts_time_next_fivedays": + case "ts_time_last_fivedays": + return 432000; + case "ts_time_next_week": + case "ts_time_last_week": + return 604800; + case "ts_time_next_month": + case "ts_time_last_month": + return 2592000; + } +} + +/** + * Function to convert the time span to a radio button id. + * @param {string} time + * @param {boolean} future + * @returns {string} + */ +function converttimetoid(time, future) { + switch (time) { + case "15778463": + return "ts_time_all"; + case "172800": + if (future) { + return "ts_time_next_twodays"; + } + return "ts_time_past_twodays"; + case "432000": + if (future) { + return "ts_time_next_fivedays"; + } + return "ts_time_last_fivedays"; + case "604800": + if (future) { + return "ts_time_next_week"; + } + return "ts_time_last_week"; + case "2592000": + if (future) { + return "ts_time_next_month"; + + } + return "ts_time_last_month"; + } +} diff --git a/block_townsquare.php b/block_townsquare.php index 22cc853..ea2604d 100644 --- a/block_townsquare.php +++ b/block_townsquare.php @@ -41,7 +41,7 @@ public function init(): void { * @return object|null The block HTML. */ public function get_content(): object { - global $OUTPUT; + global $OUTPUT, $DB, $USER; if ($this->content !== null) { return $this->content; @@ -50,10 +50,21 @@ public function get_content(): object { $controller = new contentcontroller(); $mustachedata = new stdClass(); $mustachedata->content = $controller->content; - + $mustachedata->courses = $controller->courses; + $mustachedata->helpicon = ['text' => get_string('savehelpicontext', 'block_townsquare')]; $this->content = new stdClass(); $this->content->text = $OUTPUT->render_from_template('block_townsquare/blockcontent', $mustachedata); + + // Get the user settings if available. + $usersettings = $DB->get_record('block_townsquare_preferences', ['userid' => $USER->id]); + + // Load all javascripts. $this->page->requires->js_call_amd('block_townsquare/postletter', 'init'); + $this->page->requires->js_call_amd('block_townsquare/coursefilter', 'init'); + $this->page->requires->js_call_amd('block_townsquare/timefilter', 'init'); + $this->page->requires->js_call_amd('block_townsquare/letterfilter', 'init'); + $this->page->requires->js_call_amd('block_townsquare/filtercontroller', 'init'); + $this->page->requires->js_call_amd('block_townsquare/usersettings_save', 'init', [$USER->id, $usersettings]); return $this->content; } @@ -71,4 +82,13 @@ public function applicable_formats(): array { 'my' => true, ]; } + + /** + * Returns true if this block has global config. + * + * @return bool + */ + public function has_config() { + return true; + } } diff --git a/classes/contentcontroller.php b/classes/contentcontroller.php index 8607274..c306d85 100644 --- a/classes/contentcontroller.php +++ b/classes/contentcontroller.php @@ -43,12 +43,17 @@ class contentcontroller { /** @var array letters and other content that will be shown to the user */ public array $content; + /** @var array courses that show content in townsquare (not the same as enrolled courses) */ + public array $courses; + /** * Constructor for the controller. */ public function __construct() { $this->townsquareevents = new townsquareevents(); - $this->content = $this->build_content(); + $this->courses = []; + $this->content = []; + $this->build_content(); } // Core functions. @@ -63,27 +68,33 @@ public function build_content(): array { $orientationmarkerset = false; $index = 0; $time = time(); + $appearedcourses = []; // Build a letter for each event. foreach ($this->events as $event) { // Display a orientation marker on the current date between the other events. - if (!$orientationmarkerset && isset($event->eventtype) && ( - ($event->eventtype != 'post' && $event->timestart <= $time) || - ($event->eventtype == 'post' && $event->timestart <= $time))) { + if (!$orientationmarkerset && (($event->timestart <= $time))) { $orientationmarkerset = true; $tempcontent = new orientation_marker($index, $time); $this->content[$index] = $tempcontent->export_data(); $index++; } - if (isset($event->eventtype) && $event->eventtype == 'post') { + if ($event->eventtype == 'post') { $templetter = new letter\post_letter($index, $event); - } else if (isset($event->eventtype) && $event->eventtype == 'expectcompletionon') { + } else if ($event->eventtype == 'expectcompletionon') { $templetter = new letter\activitycompletion_letter($index, $event); } else { $templetter = new letter\letter($index, $event->courseid, $event->modulename, $event->instancename, $event->name, $event->timestart, $event->coursemoduleid); } $this->content[$index] = $templetter->export_letter(); + + // Collect the courses shown in the townsquare to be able to filter them afterwards. + if (!array_key_exists($this->content[$index]['courseid'], $appearedcourses)) { + $this->courses[] = ['courseid' => $this->content[$index]['courseid'], + 'coursename' => $this->content[$index]['coursename'], ]; + $appearedcourses[$event->courseid] = true; + } $index++; } return $this->content; diff --git a/classes/external.php b/classes/external.php new file mode 100644 index 0000000..db19576 --- /dev/null +++ b/classes/external.php @@ -0,0 +1,126 @@ +. + +/** + * External townsquare API + * + * @package block_townsquare + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_townsquare; +use external_function_parameters; +use Exception; +use external_api; +use external_value; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/lib/externallib.php'); +require_once("$CFG->libdir/externallib.php"); + +/** + * Class implementing the external API, esp. for AJAX functions. + * + * @package block_townsquare + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external extends external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function record_usersettings_parameters() { + return new external_function_parameters( + [ + 'userid' => new external_value(PARAM_INT, 'the user id'), + 'timefilterpast' => new external_value(PARAM_INT, 'time span for filtering the past'), + 'timefilterfuture' => new external_value(PARAM_INT, 'time span for filtering the future'), + 'basicletter' => new external_value(PARAM_INT, 'Setting of the letter filter for basic letters'), + 'completionletter' => new external_value(PARAM_INT, 'Setting of the letter filter for completion letters'), + 'postletter' => new external_value(PARAM_INT, 'Setting of the letter filter for post letters'), + ] + ); + } + + /** + * Return the result of the record_usersettings function + * @return external_value + */ + public static function record_usersettings_returns(): external_value { + return new external_value(PARAM_BOOL, 'true if successful'); + } + + /** + * Record the user settings + * + * @param int $userid The user id + * @param int $timefilterpast Time span for filtering the past + * @param int $timefilterfuture Time span for filtering the future + * @param int $basicletter If basic letters should be shown + * @param int $completionletter If completion letters should be shown + * @param int $postletter If post letters should be shown + * @return bool + */ + public static function record_usersettings($userid, $timefilterpast, $timefilterfuture, + $basicletter, $completionletter, $postletter): bool { + global $DB; + // Parameter validation. + $params = self::validate_parameters(self::record_usersettings_parameters(), [ + 'userid' => $userid, 'timefilterpast' => $timefilterpast, 'timefilterfuture' => $timefilterfuture, + 'basicletter' => $basicletter, 'completionletter' => $completionletter, 'postletter' => $postletter, + ]); + + // Check if the user already has a record in the database. + if ($records = $DB->get_records('block_townsquare_preferences', ['userid' => $userid])) { + // If there more than a record (it only should be only one), delete all of them and insert the new one. + if (count($records) > 1) { + try { + foreach ($records as $record) { + $DB->delete_records('block_townsquare_preferences', ['id' => $record->id]); + } + } catch (Exception $e) { + + return false; + } + } else { + // Upgrade the existing record. + $record = reset($records); + $record->timefilterpast = $params['timefilterpast']; + $record->timefilterfuture = $params['timefilterfuture']; + $record->basicletter = $params['basicletter']; + $record->completionletter = $params['completionletter']; + $record->postletter = $params['postletter']; + + $DB->update_record('block_townsquare_preferences', $record); + return true; + } + } + $record = new stdClass(); + $record->userid = $params['userid']; + $record->timefilterpast = $params['timefilterpast']; + $record->timefilterfuture = $params['timefilterfuture']; + $record->basicletter = $params['basicletter']; + $record->completionletter = $params['completionletter']; + $record->postletter = $params['postletter']; + $DB->insert_record('block_townsquare_preferences', $record); + return true; + } +} diff --git a/classes/letter/activitycompletion_letter.php b/classes/letter/activitycompletion_letter.php index 3e05f60..d09e44d 100644 --- a/classes/letter/activitycompletion_letter.php +++ b/classes/letter/activitycompletion_letter.php @@ -21,11 +21,8 @@ * @copyright 2023 Tamaro Walter * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - namespace block_townsquare\letter; -use moodle_url; - /** * Class that represents an activity completion reminder. * @@ -47,12 +44,13 @@ class activitycompletion_letter extends letter { * Constructor for the activity completion letter * * @param int $contentid internal ID in the townsquare block - * @param object $calendarevent a calendar event with information, for more see classes/townsquareevents.php + * @param object $coreevent a calendar event with information, for more see classes/townsquareevents.php */ - public function __construct($contentid, $calendarevent) { - parent::__construct($contentid, $calendarevent->courseid, $calendarevent->modulename, $calendarevent->instancename, - $calendarevent->name, $calendarevent->timestart, $calendarevent->coursemoduleid); + public function __construct($contentid, $coreevent) { + parent::__construct($contentid, $coreevent->courseid, $coreevent->modulename, $coreevent->instancename, + $coreevent->name, $coreevent->timestart, $coreevent->coursemoduleid); $this->lettertype = 'activitycompletion'; + $this->lettercolor = townsquare_get_colorsetting('completionletter'); } // Functions. @@ -62,9 +60,6 @@ public function __construct($contentid, $calendarevent) { * return array */ public function export_letter(): array { - // Change the timestamp to a date. - $date = date('d.m.Y', $this->created); - return [ 'contentid' => $this->contentid, 'lettertype' => $this->lettertype, @@ -73,9 +68,11 @@ public function export_letter(): array { 'coursename' => $this->coursename, 'instancename' => $this->instancename, 'content' => $this->content, - 'created' => $date, + 'created' => date('d.m.Y', $this->created), + 'createdtimestamp' => $this->created, 'linktoactivity' => $this->linktoactivity->out(), 'linktocourse' => $this->linktocourse->out(), + 'completionlettercolor' => $this->lettercolor, ]; } diff --git a/classes/letter/letter.php b/classes/letter/letter.php index ff23081..94abf73 100644 --- a/classes/letter/letter.php +++ b/classes/letter/letter.php @@ -21,11 +21,14 @@ * @copyright 2023 Tamaro Walter * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - namespace block_townsquare\letter; - use moodle_url; +defined('MOODLE_INTERNAL') || die; + +global $CFG; +require_once($CFG->dirroot . '/blocks/townsquare/locallib.php'); + /** * Class that represents an unspecific type of content. * @@ -66,15 +69,18 @@ class letter { /** @var int Timestamp When the activity was created */ protected int $created; - /** @var bool variable for the mustache template */ - public bool $isbasic = true; - /** @var moodle_url Link to the course */ protected moodle_url $linktocourse; /** @var moodle_url The url to the activity */ protected moodle_url $linktoactivity; + /** @var bool variable for the mustache template */ + public bool $isbasic = true; + + /** @var string color of the letter. Only used by mustache */ + public string $lettercolor; + // Constructor. /** @@ -100,6 +106,7 @@ public function __construct($contentid, $courseid, $modulename, $instancename, $ $this->created = $created; $this->linktocourse = new moodle_url('/course/view.php', ['id' => $this->courseid]); $this->linktoactivity = new moodle_url('/mod/' . $modulename . '/view.php', ['id' => $cmid]); + $this->lettercolor = townsquare_get_colorsetting('basicletter'); } // Functions. @@ -109,6 +116,7 @@ public function __construct($contentid, $courseid, $modulename, $instancename, $ * @return array */ public function export_letter() { + // Change the timestamp to a date. return [ 'contentid' => $this->contentid, 'lettertype' => $this->lettertype, @@ -118,8 +126,10 @@ public function export_letter() { 'instancename' => $this->instancename, 'content' => $this->content, 'created' => date('d.m.Y', $this->created), + 'createdtimestamp' => $this->created, 'linktocourse' => $this->linktocourse->out(), 'linktoactivity' => $this->linktoactivity->out(), + 'basiclettercolor' => $this->lettercolor, ]; } } diff --git a/classes/letter/post_letter.php b/classes/letter/post_letter.php index ed021ec..f786ff7 100644 --- a/classes/letter/post_letter.php +++ b/classes/letter/post_letter.php @@ -23,11 +23,14 @@ */ namespace block_townsquare\letter; - -use moodle_exception; use moodle_url; use stdClass; +defined('MOODLE_INTERNAL') || die; + +global $CFG; +require_once($CFG->libdir . '/portfoliolib.php'); + /** * Class that represents a post from the forum or moodleoverflow module. * @@ -87,10 +90,12 @@ public function __construct($contentid, $postevent) { $this->posturls = new stdClass(); $this->lettertype = 'post'; + $this->lettercolor = townsquare_get_colorsetting('postletter'); $this->post->instanceid = $postevent->instanceid; $this->post->discussionid = $postevent->postdiscussion; $this->post->id = $postevent->postid; - $this->post->message = $postevent->postmessage; + $this->post->message = $this->format_post($postevent->postmessage, $postevent->postmessageformat, + $postevent->coursemoduleid); $this->post->discussionsubject = $postevent->discussionsubject; $this->post->parentid = $postevent->postparentid; $this->post->anonymous = $postevent->anonymous ?? false; @@ -99,6 +104,7 @@ public function __construct($contentid, $postevent) { $this->posturls->linktopost = $postevent->linktopost; $this->posturls->linktoauthor = $postevent->linktoauthor; + $this->add_anonymousattribute($postevent); $this->retrieve_profilepicture(); } @@ -122,12 +128,14 @@ public function export_letter(): array { 'authorname' => $this->author->name, 'authorpicture' => $this->author->picture, 'postid' => $this->post->id, - 'message' => format_text($this->post->message), + 'message' => $this->post->message, 'created' => date('d.m.Y', $this->created), + 'createdtimestamp' => $this->created, 'linktocourse' => $this->linktocourse->out(), 'linktoactivity' => $this->linktoactivity->out(), 'linktopost' => $this->posturls->linktopost->out(), 'linktoauthor' => $this->posturls->linktoauthor->out(), + 'postlettercolor' => $this->lettercolor, ]; } @@ -153,4 +161,29 @@ private function retrieve_profilepicture() { } } + /** + * Method to add the anonymous attribute to the post. + * @param object $postevent a post event with information,for more see classes/townsquareevents.php. + * @return void + */ + private function add_anonymousattribute($postevent): void { + if ($postevent->modulename == 'moodleoverflow') { + $this->post->anonymous = $postevent->anonymous; + } else { + $this->post->anonymous = false; + } + } + + /** + * Function to format the post message before exporting it to the mustache template. + * @param $message + * @param $messageformat + * @param $cmid + * @return string + */ + private function format_post($message, $messageformat, $cmid) { + $options = (array) portfolio_format_text_options() + ['context' => \context_module::instance($cmid)]; + return format_text($message, $messageformat, $options); + } + } diff --git a/classes/orientation_marker.php b/classes/orientation_marker.php index 9ca6024..235aebe 100644 --- a/classes/orientation_marker.php +++ b/classes/orientation_marker.php @@ -24,6 +24,11 @@ namespace block_townsquare; +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/blocks/townsquare/locallib.php'); + /** * Class that represent a orientation marker. * @@ -55,7 +60,6 @@ class orientation_marker { public function __construct($contentid, $time) { $this->contentid = $contentid; $this->today = $time; - } // Functions. @@ -72,6 +76,7 @@ public function export_data(): array { 'contentid' => $this->contentid, 'date' => $date, 'isorientationmarker' => $this->isorientationmarker, + 'orientationmarkercolor' => townsquare_get_colorsetting('orientationmarker'), ]; } diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php new file mode 100644 index 0000000..5b2633c --- /dev/null +++ b/classes/privacy/provider.php @@ -0,0 +1,173 @@ +. + + +/** + * Privacy Provider for block_townsquare. + * @package block_townsquare + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_townsquare\privacy; + +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\userlist; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\writer; + + +/** + * Class that describes the type of data that is stored. + * + * @package block_townsquare + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\plugin\provider, + \core_privacy\local\request\core_userlist_provider { + + /** + * Function that describes the type of data that is stored. + * @param collection $collection + * @return collection + */ + public static function get_metadata(collection $collection): collection { + $collection->add_database_table('block_townsquare', [ + 'userid' => 'privacy:metadata:block_townsquare_preferences:userid', + 'timefilterpast' => 'privacy:metadata:block_townsquare_preferences:timefilterpast', + 'timefilterfuture' => 'privacy:metadata:block_townsquare_preferences:timefilterfuture', + 'basicletter' => 'privacy:metadata:block_townsquare_preferences:basicletter', + 'completionletter' => 'privacy:metadata:block_townsquare_preferences:completionletter', + 'postletter' => 'privacy:metadata:block_townsquare_preferences:postletter', + ], 'privacy:metadata:block_townsquare_preferences'); + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid): contextlist { + $sql = "SELECT context.id + FROM {block_townsquare_preferences} preferences + JOIN {user} u + ON preferences.userid = u.id + JOIN {context} context + ON context.instanceid = u.id + AND context.contextlevel = :contextlevel + WHERE preferences.userid = :userid"; + + $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER]; + + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + return $contextlist; + } + + /** + * Get the list of users within a specific context. + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + * @return void + */ + public static function get_users_in_context(userlist $userlist): void { + $context = $userlist->get_context(); + + if (!$context instanceof \context_user) { + return; + } + + $sql = "SELECT userid + FROM {block_townsquare_preferences} + WHERE userid = ?"; + $params = [$context->instanceid]; + $userlist->add_from_sql('userid', $sql, $params); + } + + /** + * Export all user data for the specified user, in the specified contexts. + * @param approved_contextlist $contextlist + * @return void + */ + public static function export_user_data(approved_contextlist $contextlist): void { + global $DB; + $townsquaredata = []; + $results = $DB->get_records('block_townsquare_preferences', ['userid' => $contextlist->get_user()->id]); + + foreach ($results as $result) { + $data = new \stdClass(); + $data->userid = $result->userid; + $data->timefilterpast = $result->timefilterpast; + $data->timefilterfuture = $result->timefilterfuture; + $data->basicletter = $result->basicletter; + $data->completionletter = $result->completionletter; + $data->postletter = $result->postletter; + + $townsquaredata[] = $data; + } + if (!empty($townsquaredata)) { + writer::with_context($contextlist->current())->export_data([ + get_string('pluginname', 'block_townsquare'), ], (object) $townsquaredata); + } + } + + /** + * Delete multiple user data within a single context. + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + global $DB; + $context = $userlist->get_context(); + if ($context instanceof \context_user && + in_array($context->instanceid, $userlist->get_userids())) { + $DB->delete_records('block_townsquare_preferences', ['userid' => $context->instanceid]); + } + } + + /** + * Delete all data for all users in the specified context. + * @param \context $context The specific context to delete data for. + * @return void + * @throws \dml_exception + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + if ($context instanceof \context_user) { + $DB->delete_records('block_townsquare_preferences', ['userid' => $context->instanceid]); + } + } + + + /** + * Delete all user data for the specified user, in the specified contexts. + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + * @return void + * @throws \dml_exception + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + foreach ($contextlist as $context) { + if ($context->contextlevel == CONTEXT_USER && $contextlist->get_user()->id == $context->instanceid) { + $DB->delete_records('block_townsquare_preferences', ['userid' => $contextlist->get_user()->id]); + } + } + } +} diff --git a/classes/townsquareevents.php b/classes/townsquareevents.php index 1b20e7b..037e132 100644 --- a/classes/townsquareevents.php +++ b/classes/townsquareevents.php @@ -26,6 +26,7 @@ defined('MOODLE_INTERNAL') || die(); use context_module; +use core_component; use dml_exception; use moodle_url; use function local_townsquaresupport\townsquaresupport_get_subplugin_events; @@ -33,6 +34,7 @@ global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); require_once($CFG->dirroot . '/blocks/townsquare/lib.php'); +require_once($CFG->dirroot . '/blocks/townsquare/locallib.php'); /** * Class to get events and posts that will be shown in the townsquare block.. @@ -82,8 +84,9 @@ public function get_all_events_sorted(): array { require_once($CFG->dirroot . '/local/townsquaresupport/lib.php'); $subpluginevents = townsquaresupport_get_subplugin_events(); } - // Merge the events in a sorted order. - $events = $coreevents + $postevents + $subpluginevents; + + // Return the events in a sorted order. + $events = array_merge($coreevents, $postevents, $subpluginevents); return townsquare_mergesort($events); } @@ -163,6 +166,9 @@ public function get_postevents(): array { private function get_forumposts_from_db($courses, $timestart): array { global $DB; // Prepare params for sql statement. + if ($courses == []) { + return []; + } list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED); $params = ['courses' => $courses, 'timestart' => $timestart] + $inparamscourses; @@ -183,7 +189,8 @@ private function get_forumposts_from_db($courses, $timestart): array { posts.parent AS postparentid, posts.userid AS postuserid, posts.created AS timestart, - posts.message AS postmessage + posts.message AS postmessage, + posts.messageformat AS postmessageformat FROM {forum_posts} posts JOIN {forum_discussions} discuss ON discuss.id = posts.discussion JOIN {forum} module ON module.id = discuss.forum @@ -213,6 +220,11 @@ private function get_forumposts_from_db($courses, $timestart): array { private function get_events_from_db($timestart, $timeend, $courses): array { global $DB; + // As there are no events without courses, return an empty array. + if ($courses == []) { + return []; + } + // Due to compatability reasons, only events from supported modules are shown. $modules = ['assign', 'book', 'chat', 'choice', 'data', 'feedback', 'file', 'folder', 'forum', 'glossary', 'h5pactivity', 'imscp', 'label', 'lesson', 'lti', 'page', 'quiz', 'resource', 'scorm', 'survey', 'url', @@ -257,12 +269,12 @@ private function filter_assignment($coreevent): bool { $overduecheck = ($type == "due" || $type == "gradingdue") && ($this->timenow >= ($coreevent->timestart + 604800)); // Check if the user is someone without grading capability. - $nogradecapabilitycheck = $coreevent->eventtype == "gradingdue" && !has_capability('mod/assign:grade', + $cannotgradecheck = $coreevent->eventtype == "gradingdue" && !has_capability('mod/assign:grade', context_module::instance($coreevent->coursemoduleid)); // Check if the assignment is not open yet. $stillclosedcheck = $assignment->allowsubmissionsfromdate >= $this->timenow; - if ($overduecheck || $nogradecapabilitycheck || $stillclosedcheck) { + if ($overduecheck || $cannotgradecheck || $stillclosedcheck) { return true; } return false; diff --git a/db/install.xml b/db/install.xml new file mode 100644 index 0000000..7cb2ae0 --- /dev/null +++ b/db/install.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/db/services.php b/db/services.php new file mode 100644 index 0000000..87b6096 --- /dev/null +++ b/db/services.php @@ -0,0 +1,36 @@ +. + +/** + * Townsquare external functions and service definitions. + * + * @package block_townsquare + * @category external + * @copyright 2017 Kennet Winter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die; + +$functions = [ + 'block_townsquare_record_usersettings' => [ + 'classname' => 'block_townsquare\external', + 'methodname' => 'record_usersettings', + 'classpath' => 'blocks/townsquare/classes/external.php', + 'description' => 'Records the user settings for the townsquare block', + 'type' => 'read', + 'ajax' => true, + ], +]; diff --git a/db/upgrade.php b/db/upgrade.php index d9ae3d0..58eb841 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -35,9 +35,33 @@ function xmldb_block_townsquare_upgrade($oldversion) { $dbman = $DB->get_manager(); // For further information please read {@link https://docs.moodle.org/dev/Upgrade_API}. - // // You will also have to create the db/install.xml file by using the XMLDB Editor. // Documentation for the XMLDB Editor can be found at {@link https://docs.moodle.org/dev/XMLDB_editor}. + if ($oldversion < 2024032201) { + // Define new table block_townsquare_preferences to be created. + $table = new xmldb_table('block_townsquare_preferences'); + + // Adding fields to table block_townsquare_preferences. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('timefilterpast', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('timefilterfuture', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('basicletter', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('completionletter', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('postletter', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table block_townsquare_preferences. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Conditionally launch create table for block_townsquare_preferences. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Moodleoverflow savepoint reached. + upgrade_block_savepoint(true, 2024032201, 'townsquare'); + } + return true; } diff --git a/lang/en/block_townsquare.php b/lang/en/block_townsquare.php index fffb6bd..f5bd5a6 100644 --- a/lang/en/block_townsquare.php +++ b/lang/en/block_townsquare.php @@ -25,34 +25,67 @@ defined('MOODLE_INTERNAL') || die(); +$string['allnotifications'] = 'All notifications'; $string['assignduemessage'] = 'Assignment is due until {$a->time}'; $string['assigngradingduemessage'] = 'Assignment is due to be graded until {$a->time}'; +$string['basiclettercolor'] = 'Color for basic letters'; $string['basicletterorigin'] = 'A notification from {$a->instancename}:'; +$string['basicletters'] = 'Basic letters'; $string['chattimemessage'] = 'The next chat time is today at {$a->time}'; $string['choiceclosemessage'] = 'Please vote until {$a->time}. Afterwards the choice is closed'; $string['choiceopenmessage'] = 'Voting is possible from {$a->time} onwards'; +$string['completionlettercolor'] = 'Color for activity completion letters'; $string['completionletterorigin'] = '{$a->modulename} should be completed until today'; +$string['completionletters'] = 'Activity completions'; +$string['configbasiclettercolor'] = 'Configuration for the color of the basic notification letters'; +$string['configcompletionlettercolor'] = 'Configuration for the color of the activity completion letters'; +$string['configorientationmarkercolor'] = 'Configuration for the color of the orientation marker'; +$string['configpostlettercolor'] = 'Configuration for the color of the post letters'; +$string['coursefilter'] = 'Course filter'; $string['dataclosemessage'] = 'Please submit your entries until {$a->time}. The database closes afterwards'; $string['dataopenmessage'] = 'The database opens today'; $string['feedbackclosemessage'] = 'Writing feedback is possible until {$a->time}'; $string['feedbackopenmessage'] = 'Writing feedback is possible from {$a->time} onwards'; $string['forumduemessage'] = 'The forum is due until {$a->time}'; +$string['invalidlettertype'] = 'Invalid function parameter, please use a valid letter type'; $string['invalidmodulename'] = 'Module name is unknown or not supported'; +$string['lastfivedaysnotifications'] = 'Last five days'; +$string['lastmonthnotifications'] = 'Last month'; +$string['lasttwodaysnotifications'] = 'Last two days'; +$string['lastweeknotifications'] = 'Last week'; $string['lessonclosemessage'] = 'The lesson ends today at {$a->time}'; $string['lessonopenmessage'] = 'The lesson opens today at {$a->time}'; +$string['letterfilter'] = 'Letter filter'; +$string['nextfivedaysnotifications'] = 'Next five days'; +$string['nextmonthnotifications'] = 'Next month'; +$string['nexttwodaysnotifications'] = 'Next two days'; +$string['nextweeknotifications'] = 'Next week'; +$string['orientationmarkercolor'] = 'Color for the orientation marker'; $string['orientationmarkercontent'] = 'Hi {$a->username}, welcome to townsquare! Today is the {$a->date}'; $string['pluginname'] = 'Town Square'; $string['pluginname:addinstance'] = 'Add the Town Square block'; $string['pluginname:myaddinstance'] = 'Add the Town Square block to the dashboard'; $string['plugintitle'] = 'Townsquare block'; - +$string['postlettercolor'] = 'Color for post letters'; +$string['postletternotification'] = 'New {$a->modulename} post!'; $string['postletterorigin'] = '{$a->authorname} posted in {$a->instancename} -> {$a->discussionname}:'; - - - - +$string['postletters'] = 'Forum posts'; +$string['privacy:metadata:block_townsquare_preferences'] = 'Town Square stores the filters that a user wants to have activated.'; +$string['privacy:metadata:block_townsquare_preferences:basicletter'] = 'If the user wants to see basic letters'; +$string['privacy:metadata:block_townsquare_preferences:completionletter'] = 'If the user wants to see completion letters'; +$string['privacy:metadata:block_townsquare_preferences:postletter'] = 'If the user wants to see post letters'; +$string['privacy:metadata:block_townsquare_preferences:timefilterfuture'] = 'How far into the future the user wants to see notifications'; +$string['privacy:metadata:block_townsquare_preferences:timefilterpast'] = 'How far back the user wants to see notifications'; +$string['privacy:metadata:block_townsquare_preferences:userid'] = 'The user id'; $string['quizclosemessage'] = 'The Quiz closes today at {$a->time}'; $string['quizopenmessage'] = 'The Quiz is open from {$a->time} onwards'; +$string['savebutton'] = 'Save settings'; +$string['savehelpicontext'] = 'Save Settings for the future'; +$string['savemessage'] = 'Settings successfully saved!'; +$string['scormclosemessage'] = 'Scorm Activity closes today'; +$string['scormopenmessage'] = 'Scorm Activity opens today'; +$string['showmore'] = 'Show more'; +$string['timefilter'] = 'Time filter'; $string['workshopcloseassessment'] = 'Assessments for the workshop are due until {$a->time}'; $string['workshopclosesubmission'] = 'Please submit your work until {$a->time}. The workshop closes afterwards'; $string['workshopopenassessment'] = 'The assessment phase starts today at {$a->time}'; diff --git a/lib.php b/lib.php index 7a2fcfa..be7bf2c 100644 --- a/lib.php +++ b/lib.php @@ -22,6 +22,12 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +// Color constants from bootstrap. +define('TOWNSQUARE_BASICLETTER_DEFAULTCOLOR', '#0f6cbf'); +define('TOWNSQUARE_POSTLETTER_DEFAULTCOLOR', '#f7634d'); +define('TOWNSQUARE_COMPLETIONLETTER_DEFAULTCOLOR', '#ca3120'); +define('TOWNSQUARE_ORIENTATIONMARKER_DEFAULTCOLOR', '#6a737b'); + /** * Gets the id of all courses where the current user is enrolled * @return array @@ -103,6 +109,9 @@ function townsquare_merge(array $left, array $right): array { // Filter functions. +/** + * Filter that checks if the event needs to be filtered out for the current user because it is not available. + */ function townsquare_filter_availability($event): bool { // If there is no restriction defined, the event is available. if ($event->availability == null) { @@ -120,7 +129,7 @@ function townsquare_filter_availability($event): bool { } /** - * Filter that checks if the event needs to be filtered out for the current user because it is already completed.. + * Filter that checks if the event needs to be filtered out for the current user because it is already completed. * Applies to activity completion events. * @param object $coreevent coreevent that is checked * @return bool true if the event needs to filtered out, false if not. @@ -135,76 +144,3 @@ function townsquare_filter_activitycompletions($coreevent): bool { } return false; } - - - -// Strings adaptation functions. - -/** - * General Support function for core events. - * Can be used to modify the event content, as in some cases, core events don't have a good text in the events-datatable. - * @param object $event The event, that is being checked. - * @return void - */ -function townsquare_check_coreevent($event) { - $time = date('H:i', $event->timestart); - if ($event->modulename == 'assign') { - if ($event->eventtype == 'due') { - $event->name = get_string('assignduemessage', 'block_townsquare', ['time' => $time]); - } else if ($event->eventtype == 'gradingdue') { - $event->name = get_string('assigngradingduemessage', 'block_townsquare', ['time' => $time]); - } - } else if ($event->modulename == 'chat' && $event->eventtype == 'chattime') { - $event->name = get_string('chattimemessage', 'block_townsquare', ['time' => $time]); - } else if ($event->modulename == 'choice') { - if ($event->eventtype == 'open') { - $event->name = get_string('choiceopenmessage', 'block_townsquare', ['time' => $time]); - } else if ($event->eventtype == 'close') { - $event->name = get_string('choiceclosemessage', 'block_townsquare', ['time' => $time]); - } - } else if ($event->modulename == 'data') { - if ($event->eventtype == 'open') { - $event->name = get_string('dataopenmessage', 'block_townsquare'); - } else if ($event->eventtype == 'close') { - $event->name = get_string('dataclosemessage', 'block_townsquare', ['time' => $time]); - } - } else if ($event->modulename == 'feedback') { - if ($event->eventtype == 'open') { - $event->name = get_string('feedbackopenmessage', 'block_townsquare', ['time' => $time]); - } else if ($event->eventtype == 'close') { - $event->name = get_string('feedbackclosemessage', 'block_townsquare', ['time' => $time]); - } - } else if ($event->modulename == 'forum') { - if ($event->eventtype == 'due') { - $event->name = get_string('forumduemessage', 'block_townsquare', ['time' => $time]); - } - } else if ($event->modulename == 'lesson') { - if ($event->eventtype == 'open') { - $event->name = get_string('lessonopenmessage', 'block_townsquare', ['time' => $time]); - } else if ($event->eventtype == 'close') { - $event->name = get_string('lessonclosemessage', 'block_townsquare', ['time' => $time]); - } - } else if ($event->modulename == 'quiz') { - if ($event->eventtype == 'open') { - $event->name = get_string('quizopenmessage', 'block_townsquare', ['time' => $time]); - } else if ($event->eventtype == 'close') { - $event->name = get_string('quizclosemessage', 'block_townsquare', ['time' => $time]); - } - } else if ($event->modulename == 'scorm') { - if ($event->eventtype == 'open') { - $event->name = get_string('scormopenmessage', 'block_townsquare'); - } else if ($event->eventtype == 'close') { - $event->name = get_string('scormclosemessage', 'block_townsquare'); - } - } else if ($event->modulename == 'workshop') { - if ($event->eventtype == 'opensubmission') { - $event->name = get_string('workshopopensubmission', 'block_townsquare', ['time' => $time]); - } else if ($event->eventtype == 'closesubmission') { - $event->name = get_string('workshopclosesubmission', 'block_townsquare', ['time' => $time]); - } else if ($event->eventtype == 'openassessment') { - $event->name = get_string('workshopopenassessment', 'block_townsquare', ['time' => $time]); - } else if ($event->eventtype == 'closeassessment') { - $event->name = get_string('workshopcloseassessment', 'block_townsquare', ['time' => $time]); - } - } -} diff --git a/locallib.php b/locallib.php new file mode 100644 index 0000000..458ae32 --- /dev/null +++ b/locallib.php @@ -0,0 +1,99 @@ +. + +/** + * Internal library of functions for the townsquare block + * + * @package block_townsquare + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Function to get the color of a letter. + * + * @param string $lettertype The type of the letter that wants to retrieve its color setting. + * @return false|string The color of the letter. + * @throws moodle_exception + */ +function townsquare_get_colorsetting($lettertype): string { + switch ($lettertype) { + case 'basicletter': + return get_config('block_townsquare', 'basiclettercolor'); + case 'postletter': + return get_config('block_townsquare', 'postlettercolor'); + case 'completionletter': + return get_config('block_townsquare', 'completionlettercolor'); + case 'orientationmarker': + return get_config('block_townsquare', 'orientationmarkercolor'); + default: + throw new \moodle_exception('invalidlettertype', 'block_townsquare'); + } +} + +/** + * General Support function for core events. + * Can be used to modify the event content, as in some cases, core events don't have a good text in the events-datatable. + * @param object $event The event, that is being checked. + * @return void + */ +function townsquare_check_coreevent(&$event): void { + // Activity completion event have a own message handling (as it always has the same structure). + if ($event->eventtype == 'expectcompletionon') { + return; + } + + $time = date('H:i', $event->timestart); + + // Most modules only have open and closing events. + $opencloseevents = ['choice' , 'data', 'feedback', 'lesson', 'quiz', 'scorm']; + if (in_array($event->modulename, $opencloseevents)) { + $event->name = townsquare_get_open_close_message($event, $time); + } + + // Other core plugins have extra/other event types. + if ($event->modulename == 'assign') { + // Event type is either 'due' or 'gradingdue'. + $identifier = 'assign' . $event->eventtype . 'message'; + $event->name = get_string($identifier, 'block_townsquare', ['time' => $time]); + } else if ($event->modulename == 'chat' && $event->eventtype == 'chattime') { + $event->name = get_string('chattimemessage', 'block_townsquare', ['time' => $time]); + } else if ($event->modulename == 'forum') { + if ($event->eventtype == 'due') { + $event->name = get_string('forumduemessage', 'block_townsquare', ['time' => $time]); + } + } else if ($event->modulename == 'workshop') { + // Event type is either 'opensubmission', 'closesubmission', 'openassessment' or 'closeassessment'. + $identifier = 'workshop' . $event->eventtype; + $event->name = get_string($identifier, 'block_townsquare', ['time' => $time]); + } +} + +/** + * Helper function for the check function. Helps to reduce repetitive checks + * @param $event + * @param $pluginname + * @param $time + * @return string + */ +function townsquare_get_open_close_message($event, $time) { + if ($event->eventtype == 'open') { + return get_string($event->modulename . 'openmessage', 'block_townsquare', ['time' => $time]); + } else if ($event->eventtype == 'close') { + return get_string($event->modulename . 'closemessage', 'block_townsquare', ['time' => $time]); + } + return ''; +} diff --git a/settings.php b/settings.php new file mode 100644 index 0000000..43a254e --- /dev/null +++ b/settings.php @@ -0,0 +1,54 @@ +. + +/** + * File for the settings of moodleoverflow. + * + * @package block_townsquare + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; +global $ADMIN, $CFG; + +if ($ADMIN->fulltree) { + require_once($CFG->dirroot . '/blocks/townsquare/lib.php'); + + // Color Setting for the color that will be used on basic letters. + $settings->add(new admin_setting_configcolourpicker('block_townsquare/basiclettercolor', + get_string('basiclettercolor', 'block_townsquare'), + get_string('configbasiclettercolor', 'block_townsquare'), + TOWNSQUARE_BASICLETTER_DEFAULTCOLOR)); + + // Color Setting for the color that will be used on post letters. + $settings->add(new admin_setting_configcolourpicker('block_townsquare/postlettercolor', + get_string('postlettercolor', 'block_townsquare'), + get_string('configpostlettercolor', 'block_townsquare'), + TOWNSQUARE_POSTLETTER_DEFAULTCOLOR)); + + // Color Setting for the color that will be used on completion letters. + $settings->add(new admin_setting_configcolourpicker('block_townsquare/completionlettercolor', + get_string('completionlettercolor', 'block_townsquare'), + get_string('configcompletionlettercolor', 'block_townsquare'), + TOWNSQUARE_COMPLETIONLETTER_DEFAULTCOLOR)); + + // Color Setting for the orientation marker. + $settings->add(new admin_setting_configcolourpicker('block_townsquare/orientationmarkercolor', + get_string('orientationmarkercolor', 'block_townsquare'), + get_string('configorientationmarkercolor', 'block_townsquare'), + TOWNSQUARE_ORIENTATIONMARKER_DEFAULTCOLOR)); +} diff --git a/styles.css b/styles.css index 659c3f7..79070ac 100644 --- a/styles.css +++ b/styles.css @@ -1,99 +1,91 @@ -/* Style for the blockcontent template*/ -.townsquare_sidepanel { - background-color: #eee; - border-radius: 0.5rem; -} - -.townsquare_content { - max-height: 750px; - border-radius: 0.5rem; - overflow:scroll; - max-width: 100%; - overflow-x: hidden; -} - -/* Style for all letter or content templates */ - -.townsquareletter_header.card-header { - padding-top: 0.4rem; - padding-bottom: 0.4rem; -} - -.townsquareletter_body.card-body { - padding-top: 0.7rem; - padding-bottom: 0.6rem; -} - -.townsquareletter_top { - display: flex; -} - -.townsquareletter_course { - flex: 1; -} - -.townsquareletter_courselink:hover { - color: #fff; - text-decoration: underline; -} -.townsquareletter_courselink{ - color: #fff; - text-decoration: none; -} - -.townsquareletter_course, -.townsquareletter_date { - color: #fff; - text-shadow: 1px 1px 2px #000; -} - -.townsquare_showmore { - cursor: pointer; -} - -/* Styles for individual letters */ - -/* basicletter Style*/ - -.basicletter.card { - border-color: var(--primary); -} - -.basicletter_header.card-header { - background-color: var(--primary); -} - -/* postletter Style */ - -.postletter.card { - border-color: var(--activitycollaboration); -} - -.postletter_header.card-header { - background-color: var(--activitycollaboration); -} - -.postletter_origin { - margin-bottom: 0.4rem; -} - -/* actvitiycompletionletter Style*/ - -.completionletter.card { - border-color: var(--danger); -} - -.completionletter_header.card-header { - background-color: var(--danger); -} - -/* Style for the orientation marker */ -.orientationmarker.card { - border-color: var(--secondary); - background-color: var(--secondary); -} - -.orientationmarker_body.card-body { - padding-top: 0.6rem; -} -/*# sourceMappingURL=styles.css.map */ +/* Style for the blockcontent template*/ +.townsquare_sidepanel.col-md-3 { + background-color: #eee; + border-radius: 0.5rem; + padding-left: 0; + padding-right: 0; +} + +.townsquare_content { + max-height: 750px; + border-radius: 0.5rem; + overflow: scroll; + max-width: 100%; + overflow-x: hidden; +} + +/* Style for the side panel filter */ +.ts_usersettings_save { + display: flex; +} + +.ts_usersettings_button { + flex: 1; +} + +.ts_usersettings_message { + background-color: #eee; + display: none; +} + +.ts_button_transition { + transition: background-color 1.0s ease, color 1.0s ease; +} + +/* Style for all letter or content templates */ + +.townsquareletter_header.card-header { + padding-top: 0.4rem; + padding-bottom: 0.4rem; +} + +.townsquareletter_body.card-body { + padding-top: 0.7rem; + padding-bottom: 0.6rem; +} + +.townsquareletter_top { + display: flex; +} + +.townsquareletter_course { + flex: 1; +} + +.townsquareletter_courselink:hover { + color: #fff; + text-decoration: underline; +} +.townsquareletter_courselink { + color: #fff; + text-decoration: none; +} + +.townsquareletter_course, +.townsquareletter_date { + color: #fff; + text-shadow: 1px 1px 2px #000; +} + +.townsquare_showmore { + cursor: pointer; +} + +/* Styles for individual letters */ + +/* basicletter Style*/ + +/* postletter Style */ + +.postletter_origin { + margin-bottom: 0.4rem; +} + +/* actvitiycompletionletter Style*/ + +/* Style for the orientation marker */ + +.orientationmarker_body.card-body { + padding-top: 0.6rem; +} +/*# sourceMappingURL=styles.css.map */ diff --git a/templates/activitycompletionletter.mustache b/templates/activitycompletionletter.mustache index 72a5ca9..dbdfdf3 100644 --- a/templates/activitycompletionletter.mustache +++ b/templates/activitycompletionletter.mustache @@ -23,32 +23,37 @@ * contentid - Unique identification for every shown plugin content * isactivitycompletion - Variable for the blockcontent template to assure this template is used. * coursename - Name of the course + * courseid - Id of the course * instancename - Name of the instance * created - Due date of activity completions * linktocourse - Link to the course * linktoactivity - Link to the module instance + * completionlettercolor - Color of the letter Example (json): { "contentid": 28, "isactivitycompletion": true, "coursename": "Moodle Engineering", + "courseid": 3, "modulename": "survey", "instancename": "How difficult was the exam?", "created": "28.10.2023", "linktoactivity": "http://localhost/moodle/mod/survey/view.php?id=72", "linktocourse": "http://localhost/moodle/course/view.php?id=3" + "completionlettercolor": "#ca3120" } }} -

-
+
+
-
+
{{#str}} coursetitle, moodle, {"course": {{#quote}} {{coursename}} {{/quote}} } {{/str}}
-
+
{{created}}
diff --git a/templates/basicletter.mustache b/templates/basicletter.mustache index 622a6e0..f78d83f 100644 --- a/templates/basicletter.mustache +++ b/templates/basicletter.mustache @@ -23,33 +23,37 @@ * contentid - Unique identification for every shown plugin content * isbasic - Variable for the blockcontent template to assure this template is used. * coursename - Name of the course + * courseid - Id of the course * instancename - Name of the instance * content - Content of the letter * created - Date of the notification * linktocourse - Link to the course * linktoactivity - Link to the module instance + * basiclettercolor - Color of the letter Example (json): { "contentid": 0, "isbasic": true, "coursename": "Moodle Engineering", + "courseid": 3, "instancename": "Lesson 1: Quantum Theory", "content": "Lesson 1: Quantum Theory closes", "created": "20.12.2023", "linktocourse": "http://localhost/moodle/course/view.php?id=20", "linktoactivity": "http://localhost/moodle/mod/lesson/view.php?id=314" + "basiclettercolor": "#0f6cbf" } }} -
-
+
+
-
+
{{#str}} coursetitle, moodle, {"course": {{#quote}} {{coursename}} {{/quote}} } {{/str}}
-
+
{{created}}
diff --git a/templates/blockcontent.mustache b/templates/blockcontent.mustache index 9409dc0..57b827b 100644 --- a/templates/blockcontent.mustache +++ b/templates/blockcontent.mustache @@ -15,7 +15,7 @@ along with Moodle. If not, see . }} {{! - @template block_townsquare/main + @template block_townsquare/blockcontent This template renders the main content area for the townsquare block. @@ -74,7 +74,12 @@ }}
-
+ +
+ {{> block_townsquare/sidepanel}} +
+ +
{{#content}} {{#isorientationmarker}} diff --git a/templates/orientationmarker.mustache b/templates/orientationmarker.mustache index 89d17e4..3bfca8f 100644 --- a/templates/orientationmarker.mustache +++ b/templates/orientationmarker.mustache @@ -22,14 +22,17 @@ Variables required for this template (more variables are available for testing and future features): * contentid - Unique identification for every shown plugin content * isorientationmarker - variable for the blockcontent template to assure this template is used + * orientationmarkercolor - Color of the letter Example (json): { "contentid": 1, "isorientationmarker": true + "orientationmarkercolor": "#6a737b" } }} -
+
\ No newline at end of file diff --git a/templates/postletter.mustache b/templates/postletter.mustache index 784ed61..6f2d7e8 100644 --- a/templates/postletter.mustache +++ b/templates/postletter.mustache @@ -23,6 +23,7 @@ * contentid - Unique identification for every shown plugin content * ispost - Variable for the blockcontent template to assure this template is used. * coursename - Name of the course + * courseid - Id of the course * instancename - Name of the forum/moodleoverflow * discussionsubject - Name of the discussion * anonymous - if the post is anonymous @@ -34,12 +35,14 @@ * linktoactivity - Link to the module instance * linktopost - Link to the discussion post * linttoauthor - Link to the user + * postlettercolor - Color of the letter Example (json): { "contentid": 27, "ispost": true, "coursename": "Moodle Engineering", + "courseid": 3, "instancename": "Exam Questions", "discussionsubject": "Hour of Exam", "anonymous": false, @@ -51,18 +54,19 @@ "linktoactivity": "http://localhost/moodle/mod/moodleoverflow/view.php?id=66", "linktopost": "http://localhost/moodle/mod/moodleoverflow/discussion.php?d=4#p6", "linktoauthor": "http://localhost/moodle/user/view.php?id=2" + "postlettercolor": "#f7634d" } }} -
-
+
+
-
+
{{#str}} coursetitle, moodle, { "course": {{#quote}} {{coursename}} {{/quote}} } {{/str}}
-
+
{{created}}
@@ -93,7 +97,7 @@ - {{#str}} showmore, moodle {{/str}} + {{#str}} showmore, block_townsquare {{/str}}
diff --git a/templates/sidepanel.mustache b/templates/sidepanel.mustache new file mode 100644 index 0000000..e1ca3a7 --- /dev/null +++ b/templates/sidepanel.mustache @@ -0,0 +1,142 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_townsquare/blockcontent + + This template renders the side panel with the filters for the townsquare content. +}} +
+
+
+

+ +

+
+ +
+
+ {{#courses}} +
+ + +
+ {{/courses}} +
+
+
+ +
+
+

+ +

+
+
+
+
+ +
+ +
+ + + + +
+ +
+ + + +
+ +
+
+
+
+ +
+
+

+ +

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+ +
+
+ {{#helpicon}} + {{>core/help_icon}} + {{/helpicon}} +
+
+
+ {{#str}}savemessage, block_townsquare{{/str}} +
+ +
\ No newline at end of file diff --git a/tests/behat/behat_block_townsquare.php b/tests/behat/behat_block_townsquare.php new file mode 100644 index 0000000..3824594 --- /dev/null +++ b/tests/behat/behat_block_townsquare.php @@ -0,0 +1,64 @@ +. + +/** + * Steps definitions related with the townsquare block. + * + * @package block_townsquare + * @category test + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +use Behat\Gherkin\Node\TableNode as TableNode; +use Behat\Mink\Exception\ElementNotFoundException; +use Behat\Mink\Exception\ExpectationException; + +/** + * townsquare-related steps definitions. + * + * @package block_townsquare + * @category test + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_block_townsquare extends behat_base { + + /** + * Adds an activity completion event. + * @Given /^I add a townsquare completion event to "(?P(?:[^"]|\\")*)"$/ + * @param string $coursename, the course short name. + */ + public function i_add_an_townsquare_completion_event(string $coursename) { + global $DB; + $generator = new testing_data_generator(); + $course = $DB->get_record('course', ['shortname' => $coursename]); + $options = [ + 'completion' => COMPLETION_TRACKING_MANUAL, + 'completionexpected' => time() + 604800, + ]; + + $assignrecord = [ + 'course' => $course->id, + 'courseid' => $course->id, + 'duedate' => time() + 604800, + 'allowsubmissionsfromdate' => time() - 604800, + 'gradingduedate' => time() + 604860, + ]; + $generator->create_module('assign', $assignrecord, $options); + } +} diff --git a/tests/behat/block_townsquare_coursefilter.feature b/tests/behat/block_townsquare_coursefilter.feature new file mode 100644 index 0000000..61d728f --- /dev/null +++ b/tests/behat/block_townsquare_coursefilter.feature @@ -0,0 +1,155 @@ +@block @block_townsquare @javascript +Feature: The townsquare block allows users to see notifications from different courses + In order to enable the townsquare block + As a student + I can add the townsquare block to my dashboard + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Tamaro | Walter | student1@example.com | S1 | + And the following "courses" exist: + | fullname | shortname | category | startdate | enddate | enablecompletion | showcompletionconditions | + | Course 1 | C1 | 0 | ##yesterday## | ##now +6 months## | 1 | 1 | + | Course 2 | C2 | 0 | ##yesterday## | ##now +6 months## | 1 | 1 | + | Course 3 | C3 | 0 | ##yesterday## | ##now +6 months## | 1 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student1 | C2 | student | + | student1 | C3 | student | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | duedate | + | assign | C1 | 10 | Test assign 1 | Assign due in 2 months | ##now -2 days## | ##now +1 days## | + | assign | C2 | 11 | Test assign 2 | Assign due in 4 days | ##now -2 days## | ##now +4 days## | + | assign | C3 | 12 | Test assign 3 | Assign due in 6 days | ##now -2 days## | ##now +6 days## | + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | townsquare | System | 1 | my-index | content | + + Scenario: Test the course filter + Given I log in as "student1" + Then I should see "Test assign 1" in the "Town Square" "block" + And I should see "Test assign 2" in the "Town Square" "block" + And I should see "Test assign 3" in the "Town Square" "block" + When I click on "Course 1" "checkbox" + Then I should not see "Test assign 1" in the "Town Square" "block" + And I should see "Test assign 2" in the "Town Square" "block" + And I should see "Test assign 3" in the "Town Square" "block" + When I click on "Course 2" "checkbox" + Then I should not see "Test assign 2" in the "Town Square" "block" + And I should see "Test assign 3" in the "Town Square" "block" + When I click on "Course 3" "checkbox" + Then I should not see "Test assign 3" in the "Town Square" "block" + + + Scenario: Test the time filter + Given the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | duedate | + | assign | C1 | 13 | Test assign 4 | Assign due in 8 days | ##now -2 days## | ##now +8 days## | + | assign | C1 | 14 | Test assign 5 | Assign due in 3 months | ##now -2 days## | ##now +2 months## | + | assign | C1 | 15 | Test assign 6 | Assign due tomorrow | ##now -2 days## | ##now -1 days## | + | assign | C1 | 16 | Test assign 7 | Assign due yesterday | ##now -2 days## | ##now -4 days## | + | assign | C1 | 17 | Test assign 8 | Assign due in 2 months | ##now -2 days## | ##now -6 days## | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | timeclose | + | choice | C2 | 18 | Test choice 1 | Choice description | ##now -8 days## | ##now -8 days## | + | choice | C2 | 19 | Test choice 2 | Choice description | ##now -2 months##| ##now -2 months## | + And I log in as "student1" + And I click on "Time filter" "text" + # Test the time filter for the future + When I click on "Next two days" "text" + Then I should see "Test assign 1" in the "Town Square" "block" + And I should not see "Test assign 2" in the "Town Square" "block" + And I should not see "Choice description" in the "Town Square" "block" + When I click on "Next five days" "text" + Then I should see "Test assign 1" in the "Town Square" "block" + And I should see "Test assign 2" in the "Town Square" "block" + And I should not see "Test assign 3" in the "Town Square" "block" + And I should not see "Choice description" in the "Town Square" "block" + When I click on "Next week" "text" + Then I should see "Test assign 1" in the "Town Square" "block" + And I should see "Test assign 2" in the "Town Square" "block" + And I should see "Test assign 3" in the "Town Square" "block" + And I should not see "Test assign 4" in the "Town Square" "block" + And I should not see "Choice description" in the "Town Square" "block" + When I click on "Next month" "text" + Then I should see "Test assign 1" in the "Town Square" "block" + And I should see "Test assign 3" in the "Town Square" "block" + And I should see "Test assign 4" in the "Town Square" "block" + And I should not see "Test assign 5" in the "Town Square" "block" + And I should not see "Choice description" in the "Town Square" "block" + # All notifications button + When I click on "All notifications" "text" + Then I should see "Test assign 1" in the "Town Square" "block" + And I should see "Test assign 3" in the "Town Square" "block" + And I should see "Test assign 5" in the "Town Square" "block" + # Test time filter for the past + When I click on "Last two days" "text" + Then I should see "Test assign 6" in the "Town Square" "block" + And I should not see "Test assign 7" in the "Town Square" "block" + And I should not see "Test assign 1" in the "Town Square" "block" + When I click on "Last five days" "text" + Then I should see "Test assign 6" in the "Town Square" "block" + And I should see "Test assign 7" in the "Town Square" "block" + And I should not see "Test assign 8" in the "Town Square" "block" + And I should not see "Test assign 1" in the "Town Square" "block" + When I click on "Last week" "text" + Then I should see "Test assign 6" in the "Town Square" "block" + And I should see "Test assign 8" in the "Town Square" "block" + And I should not see "Test choice 1" in the "Town Square" "block" + When I click on "Last month" "text" + Then I should see "Test assign 6" in the "Town Square" "block" + And I should see "Test assign 8" in the "Town Square" "block" + And I should see "Test choice 1" in the "Town Square" "block" + And I should not see "Test choice 2" in the "Town Square" "block" + When I click on "All notifications" "text" + Then I should see "Test choice 2" in the "Town Square" "block" + # Test different combinations of time filters + When I click on "Next two days" "text" + And I click on "Last five days" "text" + Then I should see "Test assign 1" in the "Town Square" "block" + And I should not see "Test assign 2" in the "Town Square" "block" + And I should see "Test assign 6" in the "Town Square" "block" + And I should see "Test assign 7" in the "Town Square" "block" + And I should not see "Test assign 8" in the "Town Square" "block" + When I click on "Next week" "text" + And I click on "Last two days" "text" + Then I should see "Test assign 1" in the "Town Square" "block" + And I should see "Test assign 3" in the "Town Square" "block" + And I should not see "Test assign 4" in the "Town Square" "block" + And I should see "Test assign 6" in the "Town Square" "block" + And I should not see "Test assign 7" in the "Town Square" "block" + When I click on "Next month" "text" + And I click on "Last week" "text" + Then I should see "Test assign 1" in the "Town Square" "block" + And I should see "Test assign 4" in the "Town Square" "block" + And I should not see "Test assign 5" in the "Town Square" "block" + And I should see "Test assign 6" in the "Town Square" "block" + And I should see "Test assign 8" in the "Town Square" "block" + And I should not see "Test Choice 1" in the "Town Square" "block" + + Scenario: Test the letter filter + Given the following "activity" exists: + | course | C1 | + | activity | forum | + | name | Test forum name | + | idnumber | forum | + And the following "mod_forum > discussions" exist: + | forum | name | subject | message | + | forum | Discussion 1 | Discussion 1 | Discussion contents 1, first message | + And I add a townsquare completion event to "C1" + And I log in as "student1" + And I click on "Letter filter" "text" + Then I should see "Test forum name" in the "Town Square" "block" + And I should see "Test assign 1" in the "Town Square" "block" + And I should see "Assignment 1" in the "Town Square" "block" + When I click on "basicletter" "checkbox" + And I should not see "Test assign 1" in the "Town Square" "block" + And I should see "Test forum name" in the "Town Square" "block" + And I should see "Assignment 1" in the "Town Square" "block" + When I click on "postletter" "checkbox" + Then I should not see "Test forum name" in the "Town Square" "block" + And I should see "Assignment 1" in the "Town Square" "block" + When I click on "completionletter" "checkbox" + Then I should not see "Assignment 1" in the "Town Square" "block" diff --git a/tests/behat/block_townsquare_savepreferences.feature b/tests/behat/block_townsquare_savepreferences.feature new file mode 100644 index 0000000..b3c8c9c --- /dev/null +++ b/tests/behat/block_townsquare_savepreferences.feature @@ -0,0 +1,35 @@ +@block @block_townsquare @javascript +Feature: In the townsquare block user can save their settings in a database. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Tamaro | Walter | student1@example.com | S1 | + And the following "courses" exist: + | fullname | shortname | category | startdate | enddate | enablecompletion | showcompletionconditions | + | Course 1 | C1 | 0 | ##yesterday## | ##now +6 months## | 1 | 1 | + | Course 2 | C2 | 0 | ##yesterday## | ##now +6 months## | 1 | 1 | + | Course 3 | C3 | 0 | ##yesterday## | ##now +6 months## | 1 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student1 | C2 | student | + | student1 | C3 | student | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | duedate | + | assign | C1 | 10 | Test assign 1 | Assign due in 2 months | ##now -2 days## | ##now +1 days## | + | assign | C2 | 11 | Test assign 2 | Assign due in 4 days | ##now -2 days## | ##now +4 days## | + | assign | C3 | 12 | Test assign 3 | Assign due in 6 days | ##now -2 days## | ##now +6 days## | + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | townsquare | System | 1 | my-index | content | + + Scenario: Test the course filter + Given I log in as "student1" + Then I should see "Test assign 2" in the "Town Square" "block" + And I click on "Time filter" "text" + And I click on "Next two days" "text" + When I click on "Save settings" "text" in the "Town Square" "block" + Then I should not see "Test assign 2" in the "Town Square" "block" + And I reload the page + Then I should not see "Test assign 2" in the "Town Square" "block" \ No newline at end of file diff --git a/tests/contentcontroller_test.php b/tests/contentcontroller_test.php index 102c243..7cfcab3 100644 --- a/tests/contentcontroller_test.php +++ b/tests/contentcontroller_test.php @@ -49,7 +49,7 @@ final class contentcontroller_test extends \advanced_testcase { /** @var bool If the moodleoverflow module is available. * This Plugin can support moodleoverflow, but it is not necessary to have it installed. */ - private bool $moodleoverflowavailable; + private bool $modoverflowinstalled; // Construct functions. @@ -84,20 +84,20 @@ public function test_letters(): void { for ($i = 0; $i < $length; $i++) { // The content has an orientation marker. - if (isset(current($content)['isorientationmarker']) && current($content)['isorientationmarker']) { + if (isset(current($content)['isorientationmarker'])) { next($content); continue; } // Declare the three types of letter checks. - $postcheck = current($events)->eventtype == 'post' && current($content)['lettertype'] == 'post'; - $completioncheck = current($events)->eventtype == 'expectcompletionon' && - current($content)['lettertype'] == 'activitycompletion'; + $postcheck = $this->check_two_params(current($events)->eventtype, 'post', current($content)['lettertype'], 'post'); + $completioncheck = $this->check_two_params(current($events)->eventtype, 'expectcompletionon', + current($content)['lettertype'], 'activitycompletion'); $basiccheck = (current($events)->eventtype != 'post' && current($events)->eventtype != 'expectcompletionon') && current($content)['lettertype'] == 'basic'; - // If one of the checks fails there is a problem while creating the letters.. + // If one of the checks fails there is a problem while creating the letters. if (!$postcheck && !$completioncheck && !$basiccheck) { $result = false; break; @@ -107,7 +107,7 @@ public function test_letters(): void { next($events); } - if ($this->moodleoverflowavailable) { + if ($this->modoverflowinstalled) { $this->assertEquals(6, count($content)); } else { $this->assertEquals(5, count($content)); @@ -138,14 +138,14 @@ private function helper_course_set_up(): void { $this->testdata->fdiscussion = (object)$forumgenerator->create_discussion($record); if ($DB->get_record('modules', ['name' => 'moodleoverflow', 'visible' => 1])) { - $this->moodleoverflowavailable = true; - $moodleoverflowgenerator = $datagenerator->get_plugin_generator('mod_moodleoverflow'); + $this->modoverflowinstalled = true; + $modoverflowgenerator = $datagenerator->get_plugin_generator('mod_moodleoverflow'); $this->testdata->moodleoverflow = $datagenerator->create_module('moodleoverflow', ['course' => $this->testdata->course->id]); - $this->testdata->mdiscussion = $moodleoverflowgenerator->post_to_forum($this->testdata->moodleoverflow, + $this->testdata->mdiscussion = $modoverflowgenerator->post_to_forum($this->testdata->moodleoverflow, $this->testdata->teacher); } else { - $this->moodleoverflowavailable = false; + $this->modoverflowinstalled = false; } // Create an assign module with activity completion. @@ -162,7 +162,7 @@ private function helper_course_set_up(): void { */ private function create_assignment($allowsubmissionsdate, $duedate, $gradingduedate): object { // Create an activity completion for the assignment if wanted. - $featurecompletionmanual = [ + $options = [ 'completion' => COMPLETION_TRACKING_MANUAL, 'completionexpected' => $duedate, ]; @@ -174,6 +174,18 @@ private function create_assignment($allowsubmissionsdate, $duedate, $gradingdued 'allowsubmissionsfromdate' => $allowsubmissionsdate, 'gradingduedate' => $gradingduedate, ]; - return $this->getDataGenerator()->create_module('assign', $assignrecord, $featurecompletionmanual); + return $this->getDataGenerator()->create_module('assign', $assignrecord, $options); + } + + /** + * Little helper function to reduce cyclomatic complexity. Checks if two params equal values. + * @param mixed $param1 + * @param mixed $equal1 + * @param mixed $param2 + * @param mixed $equal2 + * @return bool + */ + private function check_two_params($param1, $equal1, $param2, $equal2) { + return $param1 == $equal1 && $param2 == $equal2; } } diff --git a/tests/coreevents_test.php b/tests/coreevents_test.php index fd03d3e..145a057 100644 --- a/tests/coreevents_test.php +++ b/tests/coreevents_test.php @@ -76,12 +76,12 @@ public function tearDown(): void { public function test_sortorder(): void { // Get the current calendar events from the teacher. - $calendarevents = $this->get_calendarevents_from_user($this->testdata->teacher); + $coreevents = $this->get_coreevents_from_user($this->testdata->teacher); // Iterate trough all posts and check if the sort order is correct. $timestamp = 9999999999; $result = true; - foreach ($calendarevents as $event) { + foreach ($coreevents as $event) { if ($timestamp >= $event->timestart) { $timestamp = $event->timestart; } else { @@ -102,11 +102,11 @@ public function test_course_deleted(): void { $DB->delete_records('course', ['id' => $this->testdata->course1->id]); // Get the post events from the teacher. - $calendarevents = $this->get_calendarevents_from_user($this->testdata->teacher); + $coreevents = $this->get_coreevents_from_user($this->testdata->teacher); // There should be no posts from the first course. $result = true; - foreach ($calendarevents as $event) { + foreach ($coreevents as $event) { if ($event->courseid == $this->testdata->course1->id) { $result = false; } @@ -120,20 +120,20 @@ public function test_course_deleted(): void { */ public function test_coursefilter(): void { // Test case 1: Posts for the teacher. - $calendarevents = $this->get_calendarevents_from_user($this->testdata->teacher); + $coreevents = $this->get_coreevents_from_user($this->testdata->teacher); // Two checks: Is every event in the course of the teacher and is the number of events correct. - $result = $this->check_eventcourses($calendarevents, enrol_get_all_users_courses($this->testdata->teacher->id, true)); + $result = $this->check_eventcourses($coreevents, enrol_get_all_users_courses($this->testdata->teacher->id, true)); $this->assertEquals(true, $result); - $this->assertEquals(5, count($calendarevents)); + $this->assertEquals(5, count($coreevents)); // Test case 2: Post for a student. - $calendarevents = $this->get_calendarevents_from_user($this->testdata->student2); + $coreevents = $this->get_coreevents_from_user($this->testdata->student2); // Two checks: Is every event in the course of the student and is the number of events correct. - $result = $this->check_eventcourses($calendarevents, enrol_get_all_users_courses($this->testdata->student2->id, true)); + $result = $this->check_eventcourses($coreevents, enrol_get_all_users_courses($this->testdata->student2->id, true)); $this->assertEquals(true, $result); - $this->assertEquals(1, count($calendarevents)); + $this->assertEquals(1, count($coreevents)); } /** @@ -147,11 +147,11 @@ public function test_assignfilter(): void { $time - 907200, $time - 907200, false); // Get the current calendar events. - $calendarevents = $this->get_calendarevents_from_user($this->testdata->student1); + $coreevents = $this->get_coreevents_from_user($this->testdata->student1); // The assignment should not appear. $result = true; - foreach ($calendarevents as $event) { + foreach ($coreevents as $event) { if ($event->coursemoduleid == $pastassignment->cmid) { $result = false; } @@ -161,7 +161,7 @@ public function test_assignfilter(): void { // Test case 2: The student should not see the gradingdue event, the teacher should see it. // First the events of the student. $result = true; - foreach ($calendarevents as $event) { + foreach ($coreevents as $event) { if ($event->eventtype == 'gradingdue') { $result = false; } @@ -169,10 +169,10 @@ public function test_assignfilter(): void { $this->assertEquals(true, $result); // Then the events of the teacher. - $calendarevents = $this->get_calendarevents_from_user($this->testdata->teacher); + $coreevents = $this->get_coreevents_from_user($this->testdata->teacher); $result = false; - foreach ($calendarevents as $event) { + foreach ($coreevents as $event) { if ($event->eventtype == 'gradingdue') { $result = true; } @@ -182,9 +182,9 @@ public function test_assignfilter(): void { // Test case 3: Assignments that are not open should not be seen. $notopenassignment = $this->create_assignment($this->testdata->course1->id, $time + 3600, $time + 604800 , $time + 604800 , false); - $calendarevents = $this->get_calendarevents_from_user($this->testdata->student1); + $coreevents = $this->get_coreevents_from_user($this->testdata->student1); $result = true; - foreach ($calendarevents as $event) { + foreach ($coreevents as $event) { if ($event->coursemoduleid == $notopenassignment->cmid) { $result = false; } @@ -198,10 +198,10 @@ public function test_assignfilter(): void { */ public function test_activitycompletion(): void { // Test case 1: The student should see the activity completion event. - $calendarevents = $this->get_calendarevents_from_user($this->testdata->student1); + $coreevents = $this->get_coreevents_from_user($this->testdata->student1); $result = false; $count = 0; - foreach ($calendarevents as $event) { + foreach ($coreevents as $event) { if ($event->eventtype == 'expectcompletionon') { $result = true; $count++; @@ -213,9 +213,9 @@ public function test_activitycompletion(): void { // Test case 2: The student marks the assignment as completed, the activity completion event should disappear. \core_completion_external::update_activity_completion_status_manually($this->testdata->assignment1->cmid, true); - $calendarevents = $this->get_calendarevents_from_user($this->testdata->student1); + $coreevents = $this->get_coreevents_from_user($this->testdata->student1); $result = true; - foreach ($calendarevents as $event) { + foreach ($coreevents as $event) { if ($event->eventtype == 'expectcompletionon') { $result = false; } @@ -273,9 +273,9 @@ private function helper_course_set_up(): void { */ private function create_assignment($courseid, $allowsubmissionsdate, $duedate, $gradingduedate, $activitycompletion): object { // Create an activity completion for the assignment if wanted. - $featurecompletionmanual = []; + $options = []; if ($activitycompletion) { - $featurecompletionmanual = [ + $options = [ 'completion' => COMPLETION_TRACKING_MANUAL, 'completionexpected' => $duedate, ]; @@ -287,7 +287,7 @@ private function create_assignment($courseid, $allowsubmissionsdate, $duedate, $ 'allowsubmissionsfromdate' => $allowsubmissionsdate, 'gradingduedate' => $gradingduedate, ]; - return $this->getDataGenerator()->create_module('assign', $assignrecord, $featurecompletionmanual); + return $this->getDataGenerator()->create_module('assign', $assignrecord, $options); } /** @@ -295,7 +295,7 @@ private function create_assignment($courseid, $allowsubmissionsdate, $duedate, $ * @param object $user The user for whom the events should be collected (townsquareevents.php uses $USER). * @return array */ - private function get_calendarevents_from_user($user): array { + private function get_coreevents_from_user($user): array { $this->setUser($user); $townsquareevents = new townsquareevents(); return $townsquareevents->get_coreevents(); diff --git a/tests/external_test.php b/tests/external_test.php new file mode 100644 index 0000000..d10c623 --- /dev/null +++ b/tests/external_test.php @@ -0,0 +1,108 @@ +. + +/** + * Unit tests for the block_townsquare. + * + * @package block_townsquare + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_townsquare; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * PHPUnit tests for testing the process of the externallib. + * + * @package block_townsquare + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \block_townsquare\external::record_usersettings + * @runTestsInSeparateProcesses + */ +final class external_test extends \advanced_testcase { + + public function setUp(): void { + $this->resetAfterTest(); + } + + /** + * Tests the record_usersettings method. + * @runInSeparateProcess + */ + public function test_record_usersettings(): void { + global $DB; + + $usersetting = new \stdClass(); + $usersetting->userid = 1; + $usersetting->timefilterpast = 432000; + $usersetting->timefilterfuture = 2592000; + $usersetting->basicletter = 0; + $usersetting->completionletter = 1; + $usersetting->postletter = 1; + + // Test Case 1: The User sets the setting for the first time. + + // Check that there is no record in the database. + $record = $DB->get_record('block_townsquare_preferences', ['userid' => $usersetting->userid]); + $this->assertEquals(false, $record); + + // Call the function to record the user settings and check, if the record is created. + $result = \block_townsquare\external::record_usersettings($usersetting->userid, + $usersetting->timefilterpast, + $usersetting->timefilterfuture, $usersetting->basicletter, + $usersetting->completionletter, $usersetting->postletter); + + $this->assertEquals(true, $result); + $record = $DB->get_record('block_townsquare_preferences', ['userid' => $usersetting->userid]); + + // Check if the record is correct. + $this->assertEquals($usersetting->timefilterpast, $record->timefilterpast); + $this->assertEquals($usersetting->timefilterfuture, $record->timefilterfuture); + $this->assertEquals($usersetting->basicletter, $record->basicletter); + $this->assertEquals($usersetting->completionletter, $record->completionletter); + $this->assertEquals($usersetting->postletter, $record->postletter); + + // Test Case 2: The User updates the settings. + $usersetting->timefilterpast = 2592000; + $usersetting->timefilterfuture = 0; + $usersetting->basicletter = 1; + $usersetting->completionletter = 0; + $usersetting->postletter = 0; + + // Call the function to record the user settings and check, if the record is created. + $result = \block_townsquare\external::record_usersettings($usersetting->userid, $usersetting->timefilterpast, + $usersetting->timefilterfuture, $usersetting->basicletter, + $usersetting->completionletter, $usersetting->postletter); + + $this->assertEquals(true, $result); + $record = $DB->get_record('block_townsquare_preferences', ['userid' => $usersetting->userid]); + + // Check if the record is correct. + $this->assertEquals($usersetting->timefilterpast, $record->timefilterpast); + $this->assertEquals($usersetting->timefilterfuture, $record->timefilterfuture); + $this->assertEquals($usersetting->basicletter, $record->basicletter); + $this->assertEquals($usersetting->completionletter, $record->completionletter); + $this->assertEquals($usersetting->postletter, $record->postletter); + + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml new file mode 100644 index 0000000..92b65f6 --- /dev/null +++ b/tests/phpunit.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + blocks/townsquare/tests + + + + diff --git a/tests/postevents_test.php b/tests/postevents_test.php index a08e337..10ca2a4 100644 --- a/tests/postevents_test.php +++ b/tests/postevents_test.php @@ -52,7 +52,7 @@ final class postevents_test extends \advanced_testcase { /** @var bool If the moodleoverflow module is available. * This Plugin can support moodleoverflow, but it is not necessary to have it installed. */ - private $moodleoverflowavailable; + private $modoverflowinstalled; // Construct functions. public function setUp(): void { @@ -62,7 +62,7 @@ public function setUp(): void { } public function tearDown(): void { - $this->testdata = null; + // $this->testdata = null; } // Tests. @@ -73,6 +73,8 @@ public function tearDown(): void { * @return void */ public function test_sortorder(): void { + $this->create_moodleoverflow_posts(); + $this->create_forum_posts(); // Get the current post events from the teacher. $posts = $this->get_postevents_from_user($this->testdata->teacher); @@ -96,9 +98,11 @@ public function test_sortorder(): void { */ public function test_module_moodleoverflow(): void { global $DB; - if (!$this->moodleoverflowavailable) { + if (!$this->modoverflowinstalled) { return; } + $this->create_forum_posts(); + $this->create_moodleoverflow_posts(); // Test case: disable moodleoverflow. $DB->delete_records('modules', ['name' => 'moodleoverflow']); @@ -124,6 +128,8 @@ public function test_module_moodleoverflow(): void { */ public function test_module_forum(): void { global $DB; + $this->create_forum_posts(); + $this->create_moodleoverflow_posts(); // Test case: disable forum. $DB->delete_records('modules', ['name' => 'forum']); @@ -139,7 +145,7 @@ public function test_module_forum(): void { } // Two Checks: The number of posts (there are 4 moodleoverflow posts) and the result. - if ($this->moodleoverflowavailable) { + if ($this->modoverflowinstalled) { $this->assertEquals(4, count($posts)); } else { $this->assertEquals(0, count($posts)); @@ -153,6 +159,8 @@ public function test_module_forum(): void { */ public function test_course_deleted(): void { global $DB; + $this->create_forum_posts(); + $this->create_moodleoverflow_posts(); // Delete the course from the database. $DB->delete_records('course', ['id' => $this->testdata->course1->id]); @@ -174,6 +182,9 @@ public function test_course_deleted(): void { * @return void */ public function test_coursefilter(): void { + $this->create_forum_posts(); + $this->create_moodleoverflow_posts(); + // Test case 1: Post for the teacher. $posts = $this->get_postevents_from_user($this->testdata->teacher);; @@ -181,7 +192,7 @@ public function test_coursefilter(): void { $result = $this->check_postcourses($posts, enrol_get_all_users_courses($this->testdata->teacher->id, true)); // Two Checks: Is the number of posts correct (no post is missing) and is every post in the course of the teacher. - if ($this->moodleoverflowavailable) { + if ($this->modoverflowinstalled) { $this->assertEquals(6, count($posts)); } else { $this->assertEquals(2, count($posts)); @@ -193,7 +204,7 @@ public function test_coursefilter(): void { $result = $this->check_postcourses($posts, enrol_get_all_users_courses($this->testdata->student1->id, true)); - if ($this->moodleoverflowavailable) { + if ($this->modoverflowinstalled) { $this->assertEquals(3, count($posts)); } else { $this->assertEquals(1, count($posts)); @@ -205,7 +216,7 @@ public function test_coursefilter(): void { $result = $this->check_postcourses($posts, enrol_get_all_users_courses($this->testdata->student2->id, true)); - if ($this->moodleoverflowavailable) { + if ($this->modoverflowinstalled) { $this->assertEquals(3, count($posts)); } else { $this->assertEquals(1, count($posts)); @@ -218,7 +229,8 @@ public function test_coursefilter(): void { * @return void */ public function test_anonymous(): void { - if (!$this->moodleoverflowavailable) { + // Only create moodleoverflowposts. + if (!$this->create_moodleoverflow_posts()) { return; } @@ -226,7 +238,7 @@ public function test_anonymous(): void { $this->make_anonymous($this->testdata->moodleoverflow1, 1); $this->make_anonymous($this->testdata->moodleoverflow2, 2); - // Get the current post events from the teacher.. + // Get the current post events from the teacher. $posts = $this->get_postevents_from_user($this->testdata->teacher); // Posts of the first moodleoverflow. @@ -239,16 +251,16 @@ public function test_anonymous(): void { // Iterate through all posts and save the posts from teacher and student. foreach ($posts as $post) { - if ($post->modulename == 'moodleoverflow' && $post->instanceid == $this->testdata->moodleoverflow1->id) { + if ($post->instanceid == $this->testdata->moodleoverflow1->id) { if ($post->postuserid == $this->testdata->teacher->id) { $firstteacherpost = $post; - } else if ($post->postuserid == $this->testdata->student1->id) { + } else { $firststudentpost = $post; } - } else if ($post->modulename == 'moodleoverflow' && $post->instanceid == $this->testdata->moodleoverflow2->id) { + } else { if ($post->postuserid == $this->testdata->teacher->id) { $secondteacherpost = $post; - } else if ($post->postuserid == $this->testdata->student2->id) { + } else { $secondstudentpost = $post; } } @@ -269,6 +281,8 @@ public function test_anonymous(): void { */ public function test_hidden(): void { global $DB; + $this->create_forum_posts(); + $this->create_moodleoverflow_posts(); // Test case 1: Hide the first forum. $cmid = get_coursemodule_from_instance('forum', $this->testdata->forum1->id)->id; $DB->update_record('course_modules', ['id' => $cmid, 'visible' => 0]); @@ -286,7 +300,7 @@ public function test_hidden(): void { $this->assertEquals(true, $result); // Test case 2: Hide the first moodleoverflow. - if ($this->moodleoverflowavailable) { + if ($this->modoverflowinstalled) { $cmid = get_coursemodule_from_instance('moodleoverflow', $this->testdata->moodleoverflow1->id)->id; $DB->update_record('course_modules', ['id' => $cmid, 'visible' => 0]); @@ -318,10 +332,7 @@ private function helper_course_set_up(): void { $datagenerator = $this->getDataGenerator(); // Create two new courses. $this->testdata->course1 = $datagenerator->create_course(); - $course1location = ['course' => $this->testdata->course1->id]; - $this->testdata->course2 = $datagenerator->create_course(); - $course2location = ['course' => $this->testdata->course2->id]; // Create a teacher and enroll the teacher in both courses. $this->testdata->teacher = $datagenerator->create_user(); @@ -334,41 +345,65 @@ private function helper_course_set_up(): void { $this->testdata->student2 = $datagenerator->create_user(); $this->getDataGenerator()->enrol_user($this->testdata->student2->id, $this->testdata->course2->id, 'student'); - // Create a moodleoverflow with 2 post in each course. (But only if it is available. + // Check if moodleoverflow is available. if ($DB->get_record('modules', ['name' => 'moodleoverflow', 'visible' => 1])) { - $this->moodleoverflowavailable = true; - $moodleoverflowgenerator = $datagenerator->get_plugin_generator('mod_moodleoverflow'); + $this->modoverflowinstalled = true; + } else { + $this->modoverflowinstalled = false; + } + } + + /** + * Helper function that creates a moodleoverflow and posts + * @return bool + */ + private function create_moodleoverflow_posts() { + // Create a moodleoverflow with 2 post in each course. + if ($this->modoverflowinstalled) { + $course1location = ['course' => $this->testdata->course1->id]; + $course2location = ['course' => $this->testdata->course2->id]; + $datagenerator = $this->getDataGenerator(); + $modoverflowgenerator = $datagenerator->get_plugin_generator('mod_moodleoverflow'); $this->testdata->moodleoverflow1 = $datagenerator->create_module('moodleoverflow', $course1location); - $this->testdata->mdiscussion1 = $moodleoverflowgenerator->post_to_forum($this->testdata->moodleoverflow1, - $this->testdata->teacher); - $this->testdata->answer1 = $moodleoverflowgenerator->reply_to_post($this->testdata->mdiscussion1[1], - $this->testdata->student1, true); + $this->testdata->mdiscussion1 = $modoverflowgenerator->post_to_forum($this->testdata->moodleoverflow1, + $this->testdata->teacher); + $this->testdata->answer1 = $modoverflowgenerator->reply_to_post($this->testdata->mdiscussion1[1], + $this->testdata->student1); $this->testdata->moodleoverflow2 = $datagenerator->create_module('moodleoverflow', $course2location); - $this->testdata->mdiscussion2 = $moodleoverflowgenerator->post_to_forum($this->testdata->moodleoverflow2, - $this->testdata->teacher); - $this->testdata->answer2 = $moodleoverflowgenerator->reply_to_post($this->testdata->mdiscussion2[1], - $this->testdata->student2, true); + $this->testdata->mdiscussion2 = $modoverflowgenerator->post_to_forum($this->testdata->moodleoverflow2, + $this->testdata->teacher); + $this->testdata->answer2 = $modoverflowgenerator->reply_to_post($this->testdata->mdiscussion2[1], + $this->testdata->student2); + return true; } else { - $this->moodleoverflowavailable = false; + return false; } + } - // Create a forum with 2 post in each course. + /** + * Helper function that creates a forum and posts. + * @return void + */ + private function create_forum_posts() { + $datagenerator = $this->getDataGenerator(); + $course1location = ['course' => $this->testdata->course1->id]; + $course2location = ['course' => $this->testdata->course2->id]; + // Create a forum with 1 post in each course. $forumgenerator = $datagenerator->get_plugin_generator('mod_forum'); $this->testdata->forum1 = $datagenerator->create_module('forum', $course1location); $record = (array)$this->testdata->forum1 + ['forum' => $this->testdata->forum1->id, - 'userid' => $this->testdata->teacher->id, ]; + 'userid' => $this->testdata->teacher->id, ]; $this->testdata->fdiscussion1 = (object)$forumgenerator->create_discussion($record); $this->testdata->forum2 = $datagenerator->create_module('forum', $course2location); $record = (array)$this->testdata->forum2 + ['forum' => $this->testdata->forum2->id, - 'userid' => $this->testdata->teacher->id, ]; + 'userid' => $this->testdata->teacher->id, ]; $this->testdata->fdiscussion2 = (object)$forumgenerator->create_discussion($record); } - /** * Makes the existing moodleoverflow anonymous. * There are 2 types of anonymous moodleoverflows: @@ -391,12 +426,21 @@ private function make_anonymous($moodleoverflow, $anonymoussetting): void { /** * Helper function to get the post events from a certain user. * @param object $user The user for whom the events should be collected (townsquareevents.php uses $USER). + * * @return array */ private function get_postevents_from_user($user): array { $this->setUser($user); $townsquareevents = new townsquareevents(); - return $townsquareevents->get_postevents(); + $allevents = $townsquareevents->get_all_events_sorted(); + $postevents = []; + + foreach ($allevents as $event) { + if ($event->eventtype == 'post') { + $postevents[] = $event; + } + } + return $postevents; } /** diff --git a/tests/privacy/provider_test.php b/tests/privacy/provider_test.php new file mode 100644 index 0000000..af5afb5 --- /dev/null +++ b/tests/privacy/provider_test.php @@ -0,0 +1,247 @@ +. + +/** + * Unit tests for the block_townsquare + * + * @package block_townsquare + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_townsquare\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\userlist; +use block_townsquare\privacy\provider; +use core_privacy\local\request\writer; +use core_privacy\tests\provider_testcase; + +/** + * PHPUnit tests for testing the functionalities of the townsquare privacy provider + * + * @package block_townsquare + * @copyright 2024 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \block_townsquare\privacy\provider + */ +final class provider_test extends provider_testcase { + + /** @var object The data that will be used for testing. + * This Class contains: + * - a course + * - a student and a teacher and context for both + */ + private $testdata; + + public function setUp(): void { + // Create a course and a user. + $this->testdata = new \stdClass(); + $this->testdata->course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); + $this->testdata->student = $this->getDataGenerator()->create_user(); + $this->testdata->studentcontext = \context_user::instance($this->testdata->student->id); + $this->testdata->teacher = $this->getDataGenerator()->create_user(); + $this->testdata->teachercontext = \context_user::instance($this->testdata->teacher->id); + $this->getDataGenerator()->enrol_user($this->testdata->teacher->id, $this->testdata->course->id, 'teacher'); + $this->getDataGenerator()->enrol_user($this->testdata->student->id, $this->testdata->course->id, 'student'); + $this->resetAfterTest(); + } + + // Test functions. + + /** + * Test if the provider gets the right context for a user. + * @return void + */ + public function test_get_contexts_for_userid(): void { + // Check that no context are found before the setting is set. + $contextlist = provider::get_contexts_for_userid($this->testdata->student->id); + $this->assertEquals(0, count($contextlist)); + + // Add a usersetting. + $this->helper_add_preference($this->testdata->student->id); + $contextlist = provider::get_contexts_for_userid($this->testdata->student->id); + + // Check the context that should be there. + $this->assertEquals(1, count($contextlist)); + $this->assertEquals($this->testdata->studentcontext, $contextlist->current()); + } + + /** + * Test getting the users in the context related to this plugin. + * @return void + */ + public function test_get_users_in_context(): void { + // Check that no users are found in the context before the setting is set. + $userlist = new userlist($this->testdata->studentcontext, 'block_townsquare'); + provider::get_users_in_context($userlist); + $this->assertEquals(0 , count($userlist)); + + // Add a usersetting. + $this->helper_add_preference($this->testdata->student->id); + provider::get_users_in_context($userlist); + + // Check that the provider fetches the right data. + $this->assertEquals(1, count($userlist)); + $this->assertEquals($this->testdata->student, $userlist->current()); + } + + + /** + * Test if the metadata is correct. + * @return void + */ + public function test_get_metadata(): void { + // Get the metadata and check if a collection exists. + $metadata = provider::get_metadata(new collection('block_townsquare')); + $collection = $metadata->get_collection(); + $this->assertEquals(1, count($collection)); + + // Check the content of the metadata. + $table = reset($collection); + $privacyfields = $table->get_privacy_fields(); + + $this->assertEquals('block_townsquare', $table->get_name()); + $this->assertEquals(6, count($privacyfields)); + $this->assertArrayHasKey('userid', $privacyfields); + $this->assertArrayHasKey('timefilterpast', $privacyfields); + $this->assertArrayHasKey('timefilterfuture', $privacyfields); + $this->assertArrayHasKey('basicletter', $privacyfields); + $this->assertArrayHasKey('completionletter', $privacyfields); + $this->assertArrayHasKey('postletter', $privacyfields); + } + + /** + * Test if user data get exported correctly. + * @return void + */ + public function test_export_user_data(): void { + global $DB; + // Add a usersetting. + $this->helper_add_preference($this->testdata->student->id); + + // Confirm that the setting is in the database. + $record = $DB->get_records('block_townsquare_preferences', ['userid' => $this->testdata->student->id]); + $this->assertEquals(1, count($record)); + + // Export the data. + $approvedlist = new approved_contextlist($this->testdata->student, 'block_townsquare', + [$this->testdata->studentcontext->id]); + provider::export_user_data($approvedlist); + $writer = writer::with_context($this->testdata->studentcontext); + $this->assertEquals(true, $writer->has_any_data()); + } + + /** + * Test if data is delete for all users within an approved contextlist + * @return void + */ + public function test_delete_data_for_all_users_in_context(): void { + global $DB; + // Add another user with a different context and add usersettings. + $this->helper_add_preference($this->testdata->student->id); + $this->helper_add_preference($this->testdata->teacher->id); + + // Try a system context deletion, which should have no effect. + provider::delete_data_for_all_users_in_context(\context_system::instance()); + $this->assertEquals(2, count($DB->get_records('block_townsquare_preferences'))); + + // Delete all data in the first users context (studentcontext). + provider::delete_data_for_all_users_in_context(\context_user::instance($this->testdata->student->id)); + + // Only students data should be deleted. + $records = $DB->get_records('block_townsquare_preferences'); + $this->assertEquals(1, count($records)); + } + + /** + * Test deleting data within an approved contextlist for a user. + * @return void + */ + public function test_delete_data_for_user(): void { + global $DB; + // Add usersettings for both user. + $this->helper_add_preference($this->testdata->student->id); + $this->helper_add_preference($this->testdata->teacher->id); + + // Try a system context deletion, which should have no effect. + provider::delete_data_for_all_users_in_context(\context_system::instance()); + $this->assertEquals(2, count($DB->get_records('block_townsquare_preferences'))); + + // Try to delete the teacher data in a students context, which should have no effect. + $approvedlist = new approved_contextlist($this->testdata->teacher, 'block_townsquare', + [$this->testdata->studentcontext->id]); + provider::delete_data_for_user($approvedlist); + $this->assertEquals(2, count($DB->get_records('block_townsquare_preferences'))); + + // Delete the teacher date in its own context. + $approvedlist = new approved_contextlist($this->testdata->teacher, 'block_townsquare', + [$this->testdata->teachercontext->id]); + provider::delete_data_for_user($approvedlist); + $record = $DB->get_record('block_townsquare_preferences', ['userid' => $this->testdata->student->id]); + $this->assertEquals(1, count($DB->get_records('block_townsquare_preferences'))); + $this->assertEquals($this->testdata->student->id, $record->userid); + } + + /** + * Test deleting data within a contest for an approved userlist. + * @return void + */ + public function test_delete_data_for_users(): void { + global $DB; + // Add usersettings for both user. + $this->helper_add_preference($this->testdata->student->id); + $this->helper_add_preference($this->testdata->teacher->id); + + // Try a system context deletion, which should have no effect. + provider::delete_data_for_all_users_in_context(\context_system::instance()); + $this->assertEquals(2, count($DB->get_records('block_townsquare_preferences'))); + + // Try to delete user data in another users context, which should have no effect.. + $approvedlist = new approved_userlist($this->testdata->studentcontext, 'block_townsquare', [$this->testdata->teacher->id]); + provider::delete_data_for_users($approvedlist); + $this->assertEquals(2, count($DB->get_records('block_townsquare_preferences'))); + + // Delete user data in the right context. + $approvedlist = new approved_userlist($this->testdata->studentcontext, 'block_townsquare', [$this->testdata->student->id]); + provider::delete_data_for_users($approvedlist); + $this->assertEquals(1, count($DB->get_records('block_townsquare_preferences'))); + $record = $DB->get_record('block_townsquare_preferences', ['userid' => $this->testdata->teacher->id]); + $this->assertEquals($this->testdata->teacher->id, $record->userid); + + } + + // Helper functions. + + /** + * Helper function that sets up the test data. + * @return void + */ + private function helper_add_preference($userid) { + global $DB; + // Create a user preference for townsquare content filter. + $this->testdata->setting = new \stdClass(); + $this->testdata->setting->userid = $userid; + $this->testdata->setting->timefilterpast = 15778463; + $this->testdata->setting->timefilterfuture = 15778463; + $this->testdata->setting->basicletter = 1; + $this->testdata->setting->completionletter = 1; + $this->testdata->setting->postletter = 0; + $DB->insert_record('block_townsquare_preferences', $this->testdata->setting); + } +} diff --git a/version.php b/version.php index 02058d9..a457ba4 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,6 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'block_townsquare'; $plugin->release = '0.1.0'; -$plugin->version = 2023092005; -$plugin->version = 2024032201; +$plugin->version = 2024070400; $plugin->requires = 2022041900; $plugin->maturity = MATURITY_ALPHA;