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"
}
}}
-
-