diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..81ba4368dd
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,53 @@
+> **Note:** Please fill out all required sections and remove irrelevant ones.
+### π Purpose of this PR:
+
+- [ ] Fixes a bug
+- [ ] Updates for a new Moodle version
+- [ ] Adds a new feature of functionality
+- [ ] Improves or enhances existing features
+- [ ] Refactoring: restructures code for better performance or maintainability
+- [ ] Testing: add missing or improve existing tests
+- [ ] Miscellaneous: code cleaning (without functional changes), documentation, configuration, ...
+
+---
+
+### π Description:
+
+Please describe the purpose of this PR in a few sentences.
+
+- What feature or bug does it address?
+- Why is this change or addition necessary?
+- What is the expected behavior after the change?
+
+---
+
+### π Checklist
+
+Please confirm the following (check all that apply):
+
+- [ ] I have `phpunit` and/or `behat` tests that cover my changes or additions.
+- [ ] Code passes the code checker without errors and warnings.
+- [ ] Code passes the moodle-ci/cd pipeline on all supported Moodle versions or the ones the plugin supports.
+- [ ] Code does not have `var_dump()` or `var_export` or any other debugging statements (or commented out code) that
+ should not appear on the productive branch.
+- [ ] Code only uses language strings instead of hard-coded strings.
+- [ ] If there are changes in the database: I updated/created the necessary upgrade steps in `db/upgrade.php` and
+ updated the `version.php`.
+- [ ] If there are changes in javascript: I build new `.min` files with the `grunt amd` command.
+- [ ] If it is a Moodle update PR: I read the release notes, updated the `version.php` and the `CHANGES.md`.
+ I ran all tests thoroughly checking for errors. I checked if bootstrap had any changes/deprecations that require
+ changes in the plugins UI.
+
+---
+
+### π Related Issues
+
+- Related to #[IssueNumber]
+
+---
+
+### π§ΎπΈπ Additional Information (like screenshots, documentation, links, etc.)
+
+Any other relevant information.
+
+---
\ No newline at end of file
diff --git a/.github/workflows/config.json b/.github/workflows/config.json
index 016b63376a..7a643bc495 100644
--- a/.github/workflows/config.json
+++ b/.github/workflows/config.json
@@ -1,11 +1,22 @@
{
- "main-moodle": "MOODLE_405_STABLE",
+ "moodle-plugin-ci": "4.5.7",
+ "main-moodle": "MOODLE_500_STABLE",
"main-php": "8.3",
- "moodle-php": {
- "MOODLE_401_STABLE": ["7.4", "8.1"],
- "MOODLE_403_STABLE": ["8.0", "8.2"],
- "MOODLE_404_STABLE": ["8.1", "8.3"],
- "MOODLE_405_STABLE": ["8.1", "8.2", "8.3"]
- },
- "moodle-plugin-ci": "4.5.5"
-}
+ "main-db": "pgsql",
+ "moodle-testmatrix": {
+ "MOODLE_401_STABLE": {
+ "php": ["8.0", "8.1"]
+ },
+ "MOODLE_404_STABLE": {
+ "php": ["8.1", "8.2", "8.3"]
+ },
+ "MOODLE_405_STABLE": {
+ "php": ["8.1", "8.2", "8.3"],
+ "db": ["pgsql", "mariadb", "mysqli"]
+ },
+ "MOODLE_500_STABLE": {
+ "php": ["8.2", "8.3", "8.4"],
+ "db": ["pgsql", "mariadb", "mysqli"]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml
index 0b2eda27b5..023398ddf0 100644
--- a/.github/workflows/moodle-ci.yml
+++ b/.github/workflows/moodle-ci.yml
@@ -7,3 +7,4 @@ jobs:
uses: learnweb/moodle-workflows-learnweb/.github/workflows/moodle-ci.yml@main
with:
allow-mustache-lint-error: true
+ allow-grunt-error: true
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100755
index 0000000000..21d35d99cd
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,10 @@
+CHANGELOG
+=========
+
+4.5.1 (2025-05-19)
+------------------
+[HOTFIX] #214
+
+4.5.0 (2025-05-06)
+------------------
+* Moodle 4.5 compatible version
\ No newline at end of file
diff --git a/amd/build/activityhelp.min.js b/amd/build/activityhelp.min.js
index a875b70b82..4e6f2f9007 100644
--- a/amd/build/activityhelp.min.js
+++ b/amd/build/activityhelp.min.js
@@ -1,4 +1,4 @@
-define("mod_moodleoverflow/activityhelp",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
+define("mod_moodleoverflow/activityhelp",["exports"],function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
/**
* Show a help string for the amount of activity column in userstats_table.php
*
@@ -6,6 +6,6 @@ define("mod_moodleoverflow/activityhelp",["exports"],(function(_exports){Object.
* @copyright 2023 Tamaro Walter
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-const Selectors_actions={showHelpIcon:'[data-action="showhelpicon"]'};_exports.init=()=>{document.addEventListener("click",(event=>{event.target.closest(Selectors_actions.showHelpIcon)&&event.preventDefault()}))}}));
+const Selectors_actions={showHelpIcon:'[data-action="showhelpicon"]'};_exports.init=()=>{document.addEventListener("click",event=>{event.target.closest(Selectors_actions.showHelpIcon)&&event.preventDefault()})}});
//# sourceMappingURL=activityhelp.min.js.map
\ No newline at end of file
diff --git a/amd/build/activityhelp.min.js.map b/amd/build/activityhelp.min.js.map
index 64d79cd463..20737ae222 100644
--- a/amd/build/activityhelp.min.js.map
+++ b/amd/build/activityhelp.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"activityhelp.min.js","sources":["../src/activityhelp.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 * Show a help string for the amount of activity column in userstats_table.php\n *\n * @module mod_moodleoverflow/activityhelp\n * @copyright 2023 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\n\nconst Selectors = {\n actions: {\n showHelpIcon: '[data-action=\"showhelpicon\"]',\n },\n};\n\n/**\n * Function that shows the help string.\n */\nexport const init = () => {\n document.addEventListener('click', event => {\n if (event.target.closest(Selectors.actions.showHelpIcon)) {\n event.preventDefault();\n }\n });\n};"],"names":["Selectors","showHelpIcon","document","addEventListener","event","target","closest","preventDefault"],"mappings":";;;;;;;;MAyBMA,kBACO,CACLC,aAAc,8CAOF,KAChBC,SAASC,iBAAiB,SAASC,QAC3BA,MAAMC,OAAOC,QAAQN,kBAAkBC,eACvCG,MAAMG"}
\ No newline at end of file
+{"version":3,"file":"activityhelp.min.js","sources":["../src/activityhelp.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 * Show a help string for the amount of activity column in userstats_table.php\n *\n * @module mod_moodleoverflow/activityhelp\n * @copyright 2023 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst Selectors = {\n actions: {\n showHelpIcon: '[data-action=\"showhelpicon\"]',\n },\n};\n\n/**\n * Function that shows the help icon string.\n */\nexport const init = () => {\n document.addEventListener('click', event => {\n if (event.target.closest(Selectors.actions.showHelpIcon)) {\n event.preventDefault();\n }\n });\n};"],"names":["Selectors","showHelpIcon","_exports","init","document","addEventListener","event","target","closest","preventDefault"],"mappings":";;;;;;;;AAuBA,MAAMA,kBACO,CACLC,aAAc,gCAapBC,SAAAC,KANkBA,KAChBC,SAASC,iBAAiB,QAASC,QAC3BA,MAAMC,OAAOC,QAAQR,kBAAkBC,eACvCK,MAAMG,mBAGhB"}
\ No newline at end of file
diff --git a/amd/build/rating.min.js b/amd/build/rating.min.js
index 0e51924c2d..93a4db92d9 100644
--- a/amd/build/rating.min.js
+++ b/amd/build/rating.min.js
@@ -1,10 +1,10 @@
-define("mod_moodleoverflow/rating",["exports","core/ajax","core/prefetch","core/str"],(function(_exports,_ajax,_prefetch,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+define("mod_moodleoverflow/rating",["exports","core/ajax","core/prefetch","core/str"],function(_exports,_ajax,_prefetch,_str){function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}
/**
* Implements rating functionality
*
* @module mod_moodleoverflow/rating
* @copyright 2022 Justus Dieckmann WWU
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(userid,allowmultiplemarks){_prefetch.default.prefetchStrings("mod_moodleoverflow",["marksolved","marknotsolved","markhelpful","marknothelpful","action_remove_upvote","action_upvote","action_remove_downvote","action_downvote"]),root.onclick=async event=>{const actionElement=event.target.closest("[data-moodleoverflow-action]");if(!actionElement)return;const action=actionElement.getAttribute("data-moodleoverflow-action"),postElement=actionElement.closest("[data-moodleoverflow-postid]"),postid=null==postElement?void 0:postElement.getAttribute("data-moodleoverflow-postid");switch(action){case"upvote":case"downvote":{const isupvote="upvote"===action;if("clicked"===actionElement.getAttribute("data-moodleoverflow-state"))await sendVote(postid,isupvote?20:10,userid),actionElement.setAttribute("data-moodleoverflow-state","notclicked"),actionElement.title=await(0,_str.get_string)("action_"+action,"mod_moodleoverflow");else{const otherAction=isupvote?"downvote":"upvote";await sendVote(postid,isupvote?2:1,userid),actionElement.setAttribute("data-moodleoverflow-state","clicked");const otherElement=postElement.querySelector('[data-moodleoverflow-action="'.concat(otherAction,'"]'));otherElement.setAttribute("data-moodleoverflow-state","notclicked"),actionElement.title=await(0,_str.get_string)("action_remove_"+action,"mod_moodleoverflow"),otherElement.title=await(0,_str.get_string)("action_"+otherAction,"mod_moodleoverflow")}}break;case"helpful":case"solved":{const isHelpful="helpful"===action,htmlclass=isHelpful?"markedhelpful":"markedsolution",shouldRemove=postElement.classList.contains(htmlclass),baseRating=isHelpful?4:3,rating=shouldRemove?10*baseRating:baseRating;if(await sendVote(postid,rating,userid),allowmultiplemarks)shouldRemove&&(postElement.classList.remove(htmlclass),actionElement.textContent=await(0,_str.get_string)("mark".concat(action),"mod_moodleoverflow"),changeStrings(htmlclass,action));else for(const el of root.querySelectorAll(".moodleoverflowpost."+htmlclass))el.classList.remove(htmlclass),el.querySelector('[data-moodleoverflow-action="'.concat(action,'"]')).textContent=await(0,_str.get_string)("mark".concat(action),"mod_moodleoverflow");shouldRemove||(postElement.classList.add(htmlclass),actionElement.textContent=await(0,_str.get_string)("marknot".concat(action),"mod_moodleoverflow"),allowmultiplemarks&&changeStrings(htmlclass,action))}}}},_ajax=_interopRequireDefault(_ajax),_prefetch=_interopRequireDefault(_prefetch);const root=document.getElementById("moodleoverflow-root");async function sendVote(postid,rating,userid){const response=await _ajax.default.call([{methodname:"mod_moodleoverflow_record_vote",args:{postid:postid,ratingid:rating}}])[0];return root.querySelectorAll('[data-moodleoverflow-userreputation="'.concat(userid,'"]')).forEach((i=>{i.textContent=response.raterreputation})),root.querySelectorAll('[data-moodleoverflow-userreputation="'.concat(response.ownerid,'"]')).forEach((i=>{i.textContent=response.ownerreputation})),root.querySelectorAll('[data-moodleoverflow-postreputation="'.concat(postid,'"]')).forEach((i=>{i.textContent=response.postrating})),response}async function changeStrings(htmlclass,action){_prefetch.default.prefetchStrings("mod_moodleoverflow",["marksolved","alsomarksolved","markhelpful","alsomarkhelpful"]);var othermarkedposts=!1;for(const el of root.querySelectorAll(".moodleoverflowpost"))if(el.classList.contains(htmlclass)){othermarkedposts=!0;break}for(const el of root.querySelectorAll(".moodleoverflowpost"))!el.classList.contains(htmlclass)&&el.querySelector('[data-moodleoverflow-action="'.concat(action,'"]'))&&(el.querySelector('[data-moodleoverflow-action="'.concat(action,'"]')).textContent=othermarkedposts?await(0,_str.get_string)("alsomark".concat(action),"mod_moodleoverflow"):await(0,_str.get_string)("mark".concat(action),"mod_moodleoverflow"))}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(userid,allowmultiplemarks){_prefetch.default.prefetchStrings("mod_moodleoverflow",["marksolved","marknotsolved","markhelpful","marknothelpful","action_remove_upvote","action_upvote","action_remove_downvote","action_downvote"]),root.onclick=async event=>{const actionElement=event.target.closest("[data-moodleoverflow-action]");if(!actionElement)return;const action=actionElement.getAttribute("data-moodleoverflow-action"),postElement=actionElement.closest("[data-moodleoverflow-postid]"),postid=null==postElement?void 0:postElement.getAttribute("data-moodleoverflow-postid");switch(action){case"upvote":case"downvote":{const isupvote="upvote"===action;if("clicked"===actionElement.getAttribute("data-moodleoverflow-state"))await sendVote(postid,isupvote?RATING_REMOVE_UPVOTE:RATING_REMOVE_DOWNVOTE,userid),actionElement.setAttribute("data-moodleoverflow-state","notclicked"),actionElement.title=await(0,_str.get_string)("action_"+action,"mod_moodleoverflow");else{const otherAction=isupvote?"downvote":"upvote";await sendVote(postid,isupvote?RATING_UPVOTE:RATING_DOWNVOTE,userid),actionElement.setAttribute("data-moodleoverflow-state","clicked");const otherElement=postElement.querySelector(`[data-moodleoverflow-action="${otherAction}"]`);otherElement.setAttribute("data-moodleoverflow-state","notclicked"),actionElement.title=await(0,_str.get_string)("action_remove_"+action,"mod_moodleoverflow"),otherElement.title=await(0,_str.get_string)("action_"+otherAction,"mod_moodleoverflow")}}break;case"helpful":case"solved":{const isHelpful="helpful"===action,htmlclass=isHelpful?"markedhelpful":"markedsolution",shouldRemove=postElement.classList.contains(htmlclass),baseRating=isHelpful?RATING_HELPFUL:RATING_SOLVED,rating=shouldRemove?10*baseRating:baseRating;if(await sendVote(postid,rating,userid),allowmultiplemarks)shouldRemove&&(postElement.classList.remove(htmlclass),actionElement.textContent=await(0,_str.get_string)(`mark${action}`,"mod_moodleoverflow"),changeStrings(htmlclass,action));else for(const el of root.querySelectorAll(".moodleoverflowpost."+htmlclass))el.classList.remove(htmlclass),el.querySelector(`[data-moodleoverflow-action="${action}"]`).textContent=await(0,_str.get_string)(`mark${action}`,"mod_moodleoverflow");shouldRemove||(postElement.classList.add(htmlclass),actionElement.textContent=await(0,_str.get_string)(`marknot${action}`,"mod_moodleoverflow"),allowmultiplemarks&&changeStrings(htmlclass,action))}}}},_ajax=_interopRequireDefault(_ajax),_prefetch=_interopRequireDefault(_prefetch);const RATING_DOWNVOTE=1,RATING_UPVOTE=2,RATING_REMOVE_DOWNVOTE=10,RATING_REMOVE_UPVOTE=20,RATING_SOLVED=3,RATING_HELPFUL=4,root=document.getElementById("moodleoverflow-root");async function sendVote(postid,rating,userid){const response=await _ajax.default.call([{methodname:"mod_moodleoverflow_record_vote",args:{postid:postid,ratingid:rating}}])[0];return root.querySelectorAll(`[data-moodleoverflow-userreputation="${userid}"]`).forEach(i=>{i.textContent=response.raterreputation}),root.querySelectorAll(`[data-moodleoverflow-userreputation="${response.ownerid}"]`).forEach(i=>{i.textContent=response.ownerreputation}),root.querySelectorAll(`[data-moodleoverflow-postreputation="${postid}"]`).forEach(i=>{i.textContent=response.postrating}),response}async function changeStrings(htmlclass,action){_prefetch.default.prefetchStrings("mod_moodleoverflow",["marksolved","alsomarksolved","markhelpful","alsomarkhelpful"]);var othermarkedposts=!1;for(const el of root.querySelectorAll(".moodleoverflowpost"))if(el.classList.contains(htmlclass)){othermarkedposts=!0;break}for(const el of root.querySelectorAll(".moodleoverflowpost"))!el.classList.contains(htmlclass)&&el.querySelector(`[data-moodleoverflow-action="${action}"]`)&&(el.querySelector(`[data-moodleoverflow-action="${action}"]`).textContent=othermarkedposts?await(0,_str.get_string)(`alsomark${action}`,"mod_moodleoverflow"):await(0,_str.get_string)(`mark${action}`,"mod_moodleoverflow"))}});
//# sourceMappingURL=rating.min.js.map
\ No newline at end of file
diff --git a/amd/build/rating.min.js.map b/amd/build/rating.min.js.map
index bd1b71de82..76825615bf 100644
--- a/amd/build/rating.min.js.map
+++ b/amd/build/rating.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"rating.min.js","sources":["../src/rating.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 * Implements rating functionality\n *\n * @module mod_moodleoverflow/rating\n * @copyright 2022 Justus Dieckmann WWU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\nimport Prefetch from 'core/prefetch';\nimport {get_string as getString} from 'core/str';\n\nconst RATING_DOWNVOTE = 1;\nconst RATING_UPVOTE = 2;\nconst RATING_REMOVE_DOWNVOTE = 10;\nconst RATING_REMOVE_UPVOTE = 20;\nconst RATING_SOLVED = 3;\nconst RATING_HELPFUL = 4;\n\nconst root = document.getElementById('moodleoverflow-root');\n\n/**\n * Send a vote via AJAX, then updates post and user ratings.\n * @param {int} postid\n * @param {int} rating\n * @param {int} userid\n * @returns {Promise<*>}\n */\nasync function sendVote(postid, rating, userid) {\n const response = await Ajax.call([{\n methodname: 'mod_moodleoverflow_record_vote',\n args: {\n postid: postid,\n ratingid: rating\n }\n }])[0];\n root.querySelectorAll(`[data-moodleoverflow-userreputation=\"${userid}\"]`).forEach((i) => {\n i.textContent = response.raterreputation;\n });\n root.querySelectorAll(`[data-moodleoverflow-userreputation=\"${response.ownerid}\"]`).forEach((i) => {\n i.textContent = response.ownerreputation;\n });\n root.querySelectorAll(`[data-moodleoverflow-postreputation=\"${postid}\"]`).forEach((i) => {\n i.textContent = response.postrating;\n });\n return response;\n}\n\n\n/**\n * Init function.\n *\n * @param {int} userid\n * @param {boolean} allowmultiplemarks // true means allowed, false means not allowed.\n *\n */\nexport function init(userid, allowmultiplemarks) {\n Prefetch.prefetchStrings('mod_moodleoverflow',\n ['marksolved', 'marknotsolved', 'markhelpful', 'marknothelpful',\n 'action_remove_upvote', 'action_upvote', 'action_remove_downvote', 'action_downvote']);\n\n root.onclick = async(event) => {\n const actionElement = event.target.closest('[data-moodleoverflow-action]');\n if (!actionElement) {\n return;\n }\n\n const action = actionElement.getAttribute('data-moodleoverflow-action');\n const postElement = actionElement.closest('[data-moodleoverflow-postid]');\n const postid = postElement?.getAttribute('data-moodleoverflow-postid');\n\n switch (action) {\n case 'upvote':\n case 'downvote': {\n const isupvote = action === 'upvote';\n if (actionElement.getAttribute('data-moodleoverflow-state') === 'clicked') {\n await sendVote(postid, isupvote ? RATING_REMOVE_UPVOTE : RATING_REMOVE_DOWNVOTE, userid);\n actionElement.setAttribute('data-moodleoverflow-state', 'notclicked');\n actionElement.title = await getString('action_' + action, 'mod_moodleoverflow');\n } else {\n const otherAction = isupvote ? 'downvote' : 'upvote';\n await sendVote(postid, isupvote ? RATING_UPVOTE : RATING_DOWNVOTE, userid);\n actionElement.setAttribute('data-moodleoverflow-state', 'clicked');\n const otherElement = postElement.querySelector(\n `[data-moodleoverflow-action=\"${otherAction}\"]`);\n otherElement.setAttribute('data-moodleoverflow-state', 'notclicked');\n actionElement.title = await getString('action_remove_' + action, 'mod_moodleoverflow');\n otherElement.title = await getString('action_' + otherAction, 'mod_moodleoverflow');\n }\n }\n break;\n case 'helpful':\n case 'solved': {\n const isHelpful = action === 'helpful';\n const htmlclass = isHelpful ? 'markedhelpful' : 'markedsolution';\n const shouldRemove = postElement.classList.contains(htmlclass);\n const baseRating = isHelpful ? RATING_HELPFUL : RATING_SOLVED;\n const rating = shouldRemove ? baseRating * 10 : baseRating;\n await sendVote(postid, rating, userid);\n\n /* If multiplemarks are not allowed (that is the default mode): delete all marks.\n else: only delete the mark if the post is being unmarked.\n\n Add a mark, if the post is being marked.\n */\n if (!allowmultiplemarks) {\n // Delete all marks in the discussion\n for (const el of root.querySelectorAll('.moodleoverflowpost.' + htmlclass)) {\n el.classList.remove(htmlclass);\n el.querySelector(`[data-moodleoverflow-action=\"${action}\"]`).textContent =\n await getString(`mark${action}`, 'mod_moodleoverflow');\n }\n } else {\n // Remove only the mark of the unmarked post.\n if (shouldRemove) {\n postElement.classList.remove(htmlclass);\n actionElement.textContent = await getString(`mark${action}`, 'mod_moodleoverflow');\n changeStrings(htmlclass, action);\n }\n }\n // If the post is being marked, mark it.\n if (!shouldRemove) {\n postElement.classList.add(htmlclass);\n actionElement.textContent = await getString(`marknot${action}`, 'mod_moodleoverflow');\n if (allowmultiplemarks) {\n changeStrings(htmlclass, action);\n }\n }\n\n }\n }\n };\n\n}\n\n/**\n * Function to change the String of the post data-action button.\n * Only used if mulitplemarks are allowed.\n * @param {string} htmlclass the class where the String is being updated\n * @param {string} action helpful or solved mark\n */\nasync function changeStrings(htmlclass, action) {\n Prefetch.prefetchStrings('mod_moodleoverflow',\n ['marksolved', 'alsomarksolved', 'markhelpful', 'alsomarkhelpful',]);\n\n // 1. Step: Are there other posts in the Discussion, that are solved/helpful?\n var othermarkedposts = false;\n for (const el of root.querySelectorAll('.moodleoverflowpost')) {\n if (el.classList.contains(htmlclass)) {\n othermarkedposts = true;\n break;\n }\n }\n // 2. Step: Change the strings of the action Button of the unmarked posts.\n for (const el of root.querySelectorAll('.moodleoverflowpost')) {\n if (!el.classList.contains(htmlclass) && el.querySelector(`[data-moodleoverflow-action=\"${action}\"]`)) {\n if (othermarkedposts) {\n el.querySelector(`[data-moodleoverflow-action=\"${action}\"]`).textContent =\n await getString(`alsomark${action}`, 'mod_moodleoverflow');\n } else {\n el.querySelector(`[data-moodleoverflow-action=\"${action}\"]`).textContent =\n await getString(`mark${action}`, 'mod_moodleoverflow');\n }\n }\n }\n}"],"names":["userid","allowmultiplemarks","prefetchStrings","root","onclick","async","actionElement","event","target","closest","action","getAttribute","postElement","postid","isupvote","sendVote","setAttribute","title","otherAction","otherElement","querySelector","isHelpful","htmlclass","shouldRemove","classList","contains","baseRating","rating","remove","textContent","changeStrings","el","querySelectorAll","add","document","getElementById","response","Ajax","call","methodname","args","ratingid","forEach","i","raterreputation","ownerid","ownerreputation","postrating","othermarkedposts"],"mappings":";;;;;;;oFAsEqBA,OAAQC,sCAChBC,gBAAgB,qBACrB,CAAC,aAAc,gBAAiB,cAAe,iBAC3C,uBAAwB,gBAAiB,yBAA0B,oBAE3EC,KAAKC,QAAUC,MAAAA,cACLC,cAAgBC,MAAMC,OAAOC,QAAQ,oCACtCH,2BAICI,OAASJ,cAAcK,aAAa,8BACpCC,YAAcN,cAAcG,QAAQ,gCACpCI,OAASD,MAAAA,mBAAAA,YAAaD,aAAa,qCAEjCD,YACC,aACA,kBACKI,SAAsB,WAAXJ,UAC+C,YAA5DJ,cAAcK,aAAa,mCACrBI,SAASF,OAAQC,SA7Dd,GADE,GA8DsEd,QACjFM,cAAcU,aAAa,4BAA6B,cACxDV,cAAcW,YAAc,mBAAU,UAAYP,OAAQ,0BACvD,OACGQ,YAAcJ,SAAW,WAAa,eACtCC,SAASF,OAAQC,SApErB,EADE,EAqE+Dd,QACnEM,cAAcU,aAAa,4BAA6B,iBAClDG,aAAeP,YAAYQ,qDACGF,mBACpCC,aAAaH,aAAa,4BAA6B,cACvDV,cAAcW,YAAc,mBAAU,iBAAmBP,OAAQ,sBACjES,aAAaF,YAAc,mBAAU,UAAYC,YAAa,iCAIjE,cACA,gBACKG,UAAuB,YAAXX,OACZY,UAAYD,UAAY,gBAAkB,iBAC1CE,aAAeX,YAAYY,UAAUC,SAASH,WAC9CI,WAAaL,UA/EZ,EADD,EAiFAM,OAASJ,aAA4B,GAAbG,WAAkBA,oBAC1CX,SAASF,OAAQc,OAAQ3B,QAO1BC,mBASGsB,eACAX,YAAYY,UAAUI,OAAON,WAC7BhB,cAAcuB,kBAAoB,iCAAiBnB,QAAU,sBAC7DoB,cAAcR,UAAWZ,kBAVxB,MAAMqB,MAAM5B,KAAK6B,iBAAiB,uBAAyBV,WAC5DS,GAAGP,UAAUI,OAAON,WACpBS,GAAGX,qDAA8CV,cAAYmB,kBACnD,iCAAiBnB,QAAU,sBAWxCa,eACDX,YAAYY,UAAUS,IAAIX,WAC1BhB,cAAcuB,kBAAoB,oCAAoBnB,QAAU,sBAC5DT,oBACA6B,cAAcR,UAAWZ,mGA1G3CP,KAAO+B,SAASC,eAAe,sCAStBpB,SAASF,OAAQc,OAAQ3B,cAC9BoC,eAAiBC,cAAKC,KAAK,CAAC,CAC9BC,WAAY,iCACZC,KAAM,CACF3B,OAAQA,OACR4B,SAAUd,WAEd,UACJxB,KAAK6B,gEAAyDhC,cAAY0C,SAASC,IAC/EA,EAAEd,YAAcO,SAASQ,mBAE7BzC,KAAK6B,gEAAyDI,SAASS,eAAaH,SAASC,IACzFA,EAAEd,YAAcO,SAASU,mBAE7B3C,KAAK6B,gEAAyDnB,cAAY6B,SAASC,IAC/EA,EAAEd,YAAcO,SAASW,cAEtBX,wBAgGIN,cAAcR,UAAWZ,0BAC3BR,gBAAgB,qBACrB,CAAC,aAAc,iBAAkB,cAAe,wBAGhD8C,kBAAmB,MAClB,MAAMjB,MAAM5B,KAAK6B,iBAAiB,0BAC/BD,GAAGP,UAAUC,SAASH,WAAY,CAClC0B,kBAAmB,YAKtB,MAAMjB,MAAM5B,KAAK6B,iBAAiB,wBAC9BD,GAAGP,UAAUC,SAASH,YAAcS,GAAGX,qDAA8CV,gBAElFqB,GAAGX,qDAA8CV,cAAYmB,YAD7DmB,uBAEU,qCAAqBtC,QAAU,4BAG/B,iCAAiBA,QAAU"}
\ No newline at end of file
+{"version":3,"file":"rating.min.js","sources":["../src/rating.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 * Implements rating functionality\n *\n * @module mod_moodleoverflow/rating\n * @copyright 2022 Justus Dieckmann WWU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\nimport Prefetch from 'core/prefetch';\nimport {get_string as getString} from 'core/str';\n\nconst RATING_DOWNVOTE = 1;\nconst RATING_UPVOTE = 2;\nconst RATING_REMOVE_DOWNVOTE = 10;\nconst RATING_REMOVE_UPVOTE = 20;\nconst RATING_SOLVED = 3;\nconst RATING_HELPFUL = 4;\n\nconst root = document.getElementById('moodleoverflow-root');\n\n/**\n * Send a vote via AJAX, then updates post and user ratings.\n * @param {int} postid\n * @param {int} rating\n * @param {int} userid\n * @returns {Promise<*>}\n */\nasync function sendVote(postid, rating, userid) {\n const response = await Ajax.call([{\n methodname: 'mod_moodleoverflow_record_vote',\n args: {\n postid: postid,\n ratingid: rating\n }\n }])[0];\n\n root.querySelectorAll(`[data-moodleoverflow-userreputation=\"${userid}\"]`).forEach((i) => {\n i.textContent = response.raterreputation;\n });\n\n root.querySelectorAll(`[data-moodleoverflow-userreputation=\"${response.ownerid}\"]`).forEach((i) => {\n i.textContent = response.ownerreputation;\n });\n\n root.querySelectorAll(`[data-moodleoverflow-postreputation=\"${postid}\"]`).forEach((i) => {\n i.textContent = response.postrating;\n });\n\n return response;\n}\n\n\n/**\n * Init function.\n *\n * @param {int} userid\n * @param {boolean} allowmultiplemarks // true means allowed, false means not allowed.\n *\n */\nexport function init(userid, allowmultiplemarks) {\n Prefetch.prefetchStrings('mod_moodleoverflow',\n ['marksolved', 'marknotsolved', 'markhelpful', 'marknothelpful',\n 'action_remove_upvote', 'action_upvote', 'action_remove_downvote', 'action_downvote']);\n\n root.onclick = async(event) => {\n const actionElement = event.target.closest('[data-moodleoverflow-action]');\n if (!actionElement) {\n return;\n }\n\n const action = actionElement.getAttribute('data-moodleoverflow-action');\n const postElement = actionElement.closest('[data-moodleoverflow-postid]');\n const postid = postElement?.getAttribute('data-moodleoverflow-postid');\n\n switch (action) {\n case 'upvote':\n case 'downvote': {\n const isupvote = action === 'upvote';\n if (actionElement.getAttribute('data-moodleoverflow-state') === 'clicked') {\n await sendVote(postid, isupvote ? RATING_REMOVE_UPVOTE : RATING_REMOVE_DOWNVOTE, userid);\n actionElement.setAttribute('data-moodleoverflow-state', 'notclicked');\n actionElement.title = await getString('action_' + action, 'mod_moodleoverflow');\n } else {\n const otherAction = isupvote ? 'downvote' : 'upvote';\n await sendVote(postid, isupvote ? RATING_UPVOTE : RATING_DOWNVOTE, userid);\n actionElement.setAttribute('data-moodleoverflow-state', 'clicked');\n const otherElement = postElement.querySelector(\n `[data-moodleoverflow-action=\"${otherAction}\"]`);\n otherElement.setAttribute('data-moodleoverflow-state', 'notclicked');\n actionElement.title = await getString('action_remove_' + action, 'mod_moodleoverflow');\n otherElement.title = await getString('action_' + otherAction, 'mod_moodleoverflow');\n }\n }\n break;\n case 'helpful':\n case 'solved': {\n const isHelpful = action === 'helpful';\n const htmlclass = isHelpful ? 'markedhelpful' : 'markedsolution';\n const shouldRemove = postElement.classList.contains(htmlclass);\n const baseRating = isHelpful ? RATING_HELPFUL : RATING_SOLVED;\n const rating = shouldRemove ? baseRating * 10 : baseRating;\n await sendVote(postid, rating, userid);\n\n /* If multiplemarks are not allowed (that is the default mode): delete all marks.\n else: only delete the mark if the post is being unmarked.\n\n Add a mark, if the post is being marked.\n */\n if (!allowmultiplemarks) {\n // Delete all marks in the discussion\n for (const el of root.querySelectorAll('.moodleoverflowpost.' + htmlclass)) {\n el.classList.remove(htmlclass);\n el.querySelector(`[data-moodleoverflow-action=\"${action}\"]`).textContent =\n await getString(`mark${action}`, 'mod_moodleoverflow');\n }\n } else {\n // Remove only the mark of the unmarked post.\n if (shouldRemove) {\n postElement.classList.remove(htmlclass);\n actionElement.textContent = await getString(`mark${action}`, 'mod_moodleoverflow');\n changeStrings(htmlclass, action);\n }\n }\n // If the post is being marked, mark it.\n if (!shouldRemove) {\n postElement.classList.add(htmlclass);\n actionElement.textContent = await getString(`marknot${action}`, 'mod_moodleoverflow');\n if (allowmultiplemarks) {\n changeStrings(htmlclass, action);\n }\n }\n\n }\n }\n };\n\n}\n\n/**\n * Function to change the String of the post data-action button.\n * Only used if multiplemarks are allowed.\n * @param {string} htmlclass the class where the String is being updated\n * @param {string} action helpful or solved mark\n */\nasync function changeStrings(htmlclass, action) {\n Prefetch.prefetchStrings('mod_moodleoverflow',\n ['marksolved', 'alsomarksolved', 'markhelpful', 'alsomarkhelpful',]);\n\n // 1. Step: Are there other posts in the Discussion, that are solved/helpful?\n var othermarkedposts = false;\n for (const el of root.querySelectorAll('.moodleoverflowpost')) {\n if (el.classList.contains(htmlclass)) {\n othermarkedposts = true;\n break;\n }\n }\n // 2. Step: Change the strings of the action Button of the unmarked posts.\n for (const el of root.querySelectorAll('.moodleoverflowpost')) {\n if (!el.classList.contains(htmlclass) && el.querySelector(`[data-moodleoverflow-action=\"${action}\"]`)) {\n if (othermarkedposts) {\n el.querySelector(`[data-moodleoverflow-action=\"${action}\"]`).textContent =\n await getString(`alsomark${action}`, 'mod_moodleoverflow');\n } else {\n el.querySelector(`[data-moodleoverflow-action=\"${action}\"]`).textContent =\n await getString(`mark${action}`, 'mod_moodleoverflow');\n }\n }\n }\n}"],"names":["_interopRequireDefault","e","__esModule","default","userid","allowmultiplemarks","Prefetch","prefetchStrings","root","onclick","async","actionElement","event","target","closest","action","getAttribute","postElement","postid","isupvote","sendVote","RATING_REMOVE_UPVOTE","RATING_REMOVE_DOWNVOTE","setAttribute","title","getString","otherAction","RATING_UPVOTE","RATING_DOWNVOTE","otherElement","querySelector","isHelpful","htmlclass","shouldRemove","classList","contains","baseRating","RATING_HELPFUL","RATING_SOLVED","rating","remove","textContent","changeStrings","el","querySelectorAll","add","_ajax","_prefetch","document","getElementById","response","Ajax","call","methodname","args","ratingid","forEach","i","raterreputation","ownerid","ownerreputation","postrating","othermarkedposts"],"mappings":"8HAuBqC,SAAAA,uBAAAC,GAAAA,OAAAA,GAAAA,EAAAC,WAAAD,EAAAE,CAAAA,QAAAF,EAAA;;;;;;;2EAmD9B,SAAcG,OAAQC,oBACzBC,UAAQH,QAACI,gBAAgB,qBACrB,CAAC,aAAc,gBAAiB,cAAe,iBAC3C,uBAAwB,gBAAiB,yBAA0B,oBAE3EC,KAAKC,QAAUC,cACX,MAAMC,cAAgBC,MAAMC,OAAOC,QAAQ,gCAC3C,IAAKH,cACD,OAGJ,MAAMI,OAASJ,cAAcK,aAAa,8BACpCC,YAAcN,cAAcG,QAAQ,gCACpCI,OAASD,uBAAAA,EAAAA,YAAaD,aAAa,8BAEzC,OAAQD,QACJ,IAAK,SACL,IAAK,WAAY,CACb,MAAMI,SAAsB,WAAXJ,OACjB,GAAgE,YAA5DJ,cAAcK,aAAa,mCACrBI,SAASF,OAAQC,SAAWE,qBAAuBC,uBAAwBlB,QACjFO,cAAcY,aAAa,4BAA6B,cACxDZ,cAAca,YAAc,EAAAC,KAAAA,YAAU,UAAYV,OAAQ,0BACvD,CACH,MAAMW,YAAcP,SAAW,WAAa,eACtCC,SAASF,OAAQC,SAAWQ,cAAgBC,gBAAiBxB,QACnEO,cAAcY,aAAa,4BAA6B,WACxD,MAAMM,aAAeZ,YAAYa,cAC7B,gCAAgCJ,iBACpCG,aAAaN,aAAa,4BAA6B,cACvDZ,cAAca,YAAc,EAAAC,KAAAA,YAAU,iBAAmBV,OAAQ,sBACjEc,aAAaL,YAAc,EAAAC,KAAAA,YAAU,UAAYC,YAAa,qBAClE,CACJ,CACA,MACA,IAAK,UACL,IAAK,SAAU,CACX,MAAMK,UAAuB,YAAXhB,OACZiB,UAAYD,UAAY,gBAAkB,iBAC1CE,aAAehB,YAAYiB,UAAUC,SAASH,WAC9CI,WAAaL,UAAYM,eAAiBC,cAC1CC,OAASN,aAA4B,GAAbG,WAAkBA,WAQhD,SAPMhB,SAASF,OAAQqB,OAAQnC,QAO1BC,mBASG4B,eACAhB,YAAYiB,UAAUM,OAAOR,WAC7BrB,cAAc8B,kBAAoB,EAAAhB,KAAAA,YAAU,OAAOV,SAAU,sBAC7D2B,cAAcV,UAAWjB,cAV7B,IAAK,MAAM4B,MAAMnC,KAAKoC,iBAAiB,uBAAyBZ,WAC5DW,GAAGT,UAAUM,OAAOR,WACpBW,GAAGb,cAAc,gCAAgCf,YAAY0B,kBACnD,EAAAhB,iBAAU,OAAOV,SAAU,sBAWxCkB,eACDhB,YAAYiB,UAAUW,IAAIb,WAC1BrB,cAAc8B,kBAAoB,EAAAhB,KAAAA,YAAU,UAAUV,SAAU,sBAC5DV,oBACAqC,cAAcV,UAAWjB,QAIrC,GAIZ,EAjIA+B,MAAA9C,uBAAA8C,OACAC,UAAA/C,uBAAA+C,WAGA,MAAMnB,gBAAkB,EAClBD,cAAgB,EAChBL,uBAAyB,GACzBD,qBAAuB,GACvBiB,cAAgB,EAChBD,eAAiB,EAEjB7B,KAAOwC,SAASC,eAAe,uBASrCvC,eAAeU,SAASF,OAAQqB,OAAQnC,QACpC,MAAM8C,eAAiBC,cAAKC,KAAK,CAAC,CAC9BC,WAAY,iCACZC,KAAM,CACFpC,OAAQA,OACRqC,SAAUhB,WAEd,GAcJ,OAZA/B,KAAKoC,iBAAiB,wCAAwCxC,YAAYoD,QAASC,IAC/EA,EAAEhB,YAAcS,SAASQ,kBAG7BlD,KAAKoC,iBAAiB,wCAAwCM,SAASS,aAAaH,QAASC,IACzFA,EAAEhB,YAAcS,SAASU,kBAG7BpD,KAAKoC,iBAAiB,wCAAwC1B,YAAYsC,QAASC,IAC/EA,EAAEhB,YAAcS,SAASW,aAGtBX,QACX,CA+FAxC,eAAegC,cAAcV,UAAWjB,QACpCT,UAAAA,QAASC,gBAAgB,qBACrB,CAAC,aAAc,iBAAkB,cAAe,oBAGpD,IAAIuD,kBAAmB,EACvB,IAAK,MAAMnB,MAAMnC,KAAKoC,iBAAiB,uBACnC,GAAID,GAAGT,UAAUC,SAASH,WAAY,CAClC8B,kBAAmB,EACnB,KACJ,CAGJ,IAAK,MAAMnB,MAAMnC,KAAKoC,iBAAiB,wBAC9BD,GAAGT,UAAUC,SAASH,YAAcW,GAAGb,cAAc,gCAAgCf,cAElF4B,GAAGb,cAAc,gCAAgCf,YAAY0B,YAD7DqB,uBAEU,EAAArC,iBAAU,WAAWV,SAAU,4BAG/B,EAAAU,iBAAU,OAAOV,SAAU,sBAIrD,CAAC"}
\ No newline at end of file
diff --git a/amd/build/reviewing.min.js b/amd/build/reviewing.min.js
index 7dc1ac1e4f..d027cc1635 100644
--- a/amd/build/reviewing.min.js
+++ b/amd/build/reviewing.min.js
@@ -1,10 +1,10 @@
-define("mod_moodleoverflow/reviewing",["exports","core/ajax","core/prefetch","core/templates","core/str"],(function(_exports,_ajax,_prefetch,_templates,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+define("mod_moodleoverflow/reviewing",["exports","core/ajax","core/prefetch","core/templates","core/str"],function(_exports,_ajax,_prefetch,_templates,_str){function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}
/**
* Implements reviewing functionality
*
* @module mod_moodleoverflow/reviewing
* @copyright 2022 Justus Dieckmann WWU
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(){_prefetch.default.prefetchTemplates(["mod_moodleoverflow/reject_post_form","mod_moodleoverflow/review_buttons"]),_prefetch.default.prefetchStrings("mod_moodleoverflow",["post_was_approved","jump_to_next_post_needing_review","there_are_no_posts_needing_review","post_was_rejected"]);document.getElementById("moodleoverflow-posts").onclick=async e=>{const action=e.target.getAttribute("data-moodleoverflow-action");if(!action)return;const post=e.target.closest("*[data-moodleoverflow-postid]"),reviewRow=e.target.closest(".reviewrow"),postID=post.getAttribute("data-moodleoverflow-postid");if("approve"===action){reviewRow.innerHTML=".";const nextPostURL=await _ajax.default.call([{methodname:"mod_moodleoverflow_review_approve_post",args:{postid:postID}}])[0];let message=await(0,_str.get_string)("post_was_approved","mod_moodleoverflow")+" ";message+=nextPostURL?'')+await(0,_str.get_string)("jump_to_next_post_needing_review","mod_moodleoverflow")+"":await(0,_str.get_string)("there_are_no_posts_needing_review","mod_moodleoverflow"),reviewRow.innerHTML=message,post.classList.remove("pendingreview")}else if("reject"===action)reviewRow.innerHTML=".",reviewRow.innerHTML=await _templates.default.render("mod_moodleoverflow/reject_post_form",{});else if("reject-submit"===action){const rejectMessage=post.querySelector("textarea.reject-reason").value.toString().trim();reviewRow.innerHTML=".";const args={postid:postID,reason:rejectMessage||null},nextPostURL=await _ajax.default.call([{methodname:"mod_moodleoverflow_review_reject_post",args:args}])[0];let message=await(0,_str.get_string)("post_was_rejected","mod_moodleoverflow")+" ";message+=nextPostURL?'')+await(0,_str.get_string)("jump_to_next_post_needing_review","mod_moodleoverflow")+"":await(0,_str.get_string)("there_are_no_posts_needing_review","mod_moodleoverflow"),reviewRow.innerHTML=message}else"reject-cancel"===action&&(reviewRow.innerHTML=".",reviewRow.innerHTML=await _templates.default.render("mod_moodleoverflow/review_buttons",{}))}},_ajax=_interopRequireDefault(_ajax),_prefetch=_interopRequireDefault(_prefetch),_templates=_interopRequireDefault(_templates)}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(){_prefetch.default.prefetchTemplates(["mod_moodleoverflow/reject_post_form","mod_moodleoverflow/review_buttons"]),_prefetch.default.prefetchStrings("mod_moodleoverflow",["post_was_approved","jump_to_next_post_needing_review","there_are_no_posts_needing_review","post_was_rejected"]);document.getElementById("moodleoverflow-posts").onclick=async e=>{const action=e.target.getAttribute("data-moodleoverflow-action");if(!action)return;const post=e.target.closest("*[data-moodleoverflow-postid]"),reviewRow=e.target.closest(".reviewrow"),postID=post.getAttribute("data-moodleoverflow-postid");if("approve"===action){reviewRow.innerHTML=".";const nextPostURL=await _ajax.default.call([{methodname:"mod_moodleoverflow_review_approve_post",args:{postid:postID}}])[0];let message=await(0,_str.get_string)("post_was_approved","mod_moodleoverflow")+" ";message+=nextPostURL?``+await(0,_str.get_string)("jump_to_next_post_needing_review","mod_moodleoverflow")+"":await(0,_str.get_string)("there_are_no_posts_needing_review","mod_moodleoverflow"),reviewRow.innerHTML=message,post.classList.remove("pendingreview")}else if("reject"===action)reviewRow.innerHTML=".",reviewRow.innerHTML=await _templates.default.render("mod_moodleoverflow/reject_post_form",{});else if("reject-submit"===action){const rejectMessage=post.querySelector("textarea.reject-reason").value.toString().trim();reviewRow.innerHTML=".";const args={postid:postID,reason:rejectMessage||null},nextPostURL=await _ajax.default.call([{methodname:"mod_moodleoverflow_review_reject_post",args:args}])[0];let message=await(0,_str.get_string)("post_was_rejected","mod_moodleoverflow")+" ";message+=nextPostURL?``+await(0,_str.get_string)("jump_to_next_post_needing_review","mod_moodleoverflow")+"":await(0,_str.get_string)("there_are_no_posts_needing_review","mod_moodleoverflow"),reviewRow.innerHTML=message}else"reject-cancel"===action&&(reviewRow.innerHTML=".",reviewRow.innerHTML=await _templates.default.render("mod_moodleoverflow/review_buttons",{}))}},_ajax=_interopRequireDefault(_ajax),_prefetch=_interopRequireDefault(_prefetch),_templates=_interopRequireDefault(_templates)});
//# sourceMappingURL=reviewing.min.js.map
\ No newline at end of file
diff --git a/amd/build/reviewing.min.js.map b/amd/build/reviewing.min.js.map
index d5002e736d..37e6a1b752 100644
--- a/amd/build/reviewing.min.js.map
+++ b/amd/build/reviewing.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"reviewing.min.js","sources":["../src/reviewing.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 * Implements reviewing functionality\n *\n * @module mod_moodleoverflow/reviewing\n * @copyright 2022 Justus Dieckmann WWU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\nimport Prefetch from 'core/prefetch';\nimport Templates from 'core/templates';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Init function.\n */\nexport function init() {\n Prefetch.prefetchTemplates(['mod_moodleoverflow/reject_post_form', 'mod_moodleoverflow/review_buttons']);\n Prefetch.prefetchStrings('mod_moodleoverflow',\n ['post_was_approved', 'jump_to_next_post_needing_review', 'there_are_no_posts_needing_review', 'post_was_rejected']);\n\n const root = document.getElementById('moodleoverflow-posts');\n root.onclick = async(e) => {\n const action = e.target.getAttribute('data-moodleoverflow-action');\n\n if (!action) {\n return;\n }\n\n const post = e.target.closest('*[data-moodleoverflow-postid]');\n const reviewRow = e.target.closest('.reviewrow');\n const postID = post.getAttribute('data-moodleoverflow-postid');\n\n if (action === 'approve') {\n reviewRow.innerHTML = '.';\n const nextPostURL = await Ajax.call([{\n methodname: 'mod_moodleoverflow_review_approve_post',\n args: {\n postid: postID,\n }\n }])[0];\n\n let message = await getString('post_was_approved', 'mod_moodleoverflow') + ' ';\n if (nextPostURL) {\n message += ``\n + await getString('jump_to_next_post_needing_review', 'mod_moodleoverflow')\n + \"\";\n } else {\n message += await getString('there_are_no_posts_needing_review', 'mod_moodleoverflow');\n }\n reviewRow.innerHTML = message;\n post.classList.remove(\"pendingreview\");\n } else if (action === 'reject') {\n reviewRow.innerHTML = '.';\n reviewRow.innerHTML = await Templates.render('mod_moodleoverflow/reject_post_form', {});\n } else if (action === 'reject-submit') {\n const rejectMessage = post.querySelector('textarea.reject-reason').value.toString().trim();\n reviewRow.innerHTML = '.';\n const args = {\n postid: postID,\n reason: rejectMessage ? rejectMessage : null\n };\n const nextPostURL = await Ajax.call([{\n methodname: 'mod_moodleoverflow_review_reject_post',\n args: args\n }])[0];\n\n let message = await getString('post_was_rejected', 'mod_moodleoverflow') + ' ';\n if (nextPostURL) {\n message += ``\n + await getString('jump_to_next_post_needing_review', 'mod_moodleoverflow')\n + \"\";\n } else {\n message += await getString('there_are_no_posts_needing_review', 'mod_moodleoverflow');\n }\n reviewRow.innerHTML = message;\n } else if (action === 'reject-cancel') {\n reviewRow.innerHTML = '.';\n reviewRow.innerHTML = await Templates.render('mod_moodleoverflow/review_buttons', {});\n }\n };\n}"],"names":["prefetchTemplates","prefetchStrings","document","getElementById","onclick","async","action","e","target","getAttribute","post","closest","reviewRow","postID","innerHTML","nextPostURL","Ajax","call","methodname","args","postid","message","classList","remove","Templates","render","rejectMessage","querySelector","value","toString","trim","reason"],"mappings":";;;;;;;wGA+BaA,kBAAkB,CAAC,sCAAuC,wDAC1DC,gBAAgB,qBACrB,CAAC,oBAAqB,mCAAoC,oCAAqC,sBAEtFC,SAASC,eAAe,wBAChCC,QAAUC,MAAAA,UACLC,OAASC,EAAEC,OAAOC,aAAa,kCAEhCH,oBAICI,KAAOH,EAAEC,OAAOG,QAAQ,iCACxBC,UAAYL,EAAEC,OAAOG,QAAQ,cAC7BE,OAASH,KAAKD,aAAa,iCAElB,YAAXH,OAAsB,CACtBM,UAAUE,UAAY,UAChBC,kBAAoBC,cAAKC,KAAK,CAAC,CACjCC,WAAY,yCACZC,KAAM,CACFC,OAAQP,WAEZ,OAEAQ,cAAgB,mBAAU,oBAAqB,sBAAwB,IAEvEA,SADAN,YACW,mBAAYA,wBACX,mBAAU,mCAAoC,sBACpD,aAEW,mBAAU,oCAAqC,sBAEpEH,UAAUE,UAAYO,QACtBX,KAAKY,UAAUC,OAAO,sBACnB,GAAe,WAAXjB,OACPM,UAAUE,UAAY,IACtBF,UAAUE,gBAAkBU,mBAAUC,OAAO,sCAAuC,SACjF,GAAe,kBAAXnB,OAA4B,OAC7BoB,cAAgBhB,KAAKiB,cAAc,0BAA0BC,MAAMC,WAAWC,OACpFlB,UAAUE,UAAY,UAChBK,KAAO,CACTC,OAAQP,OACRkB,OAAQL,eAAgC,MAEtCX,kBAAoBC,cAAKC,KAAK,CAAC,CACjCC,WAAY,wCACZC,KAAMA,QACN,OAEAE,cAAgB,mBAAU,oBAAqB,sBAAwB,IAEvEA,SADAN,YACW,mBAAYA,wBACX,mBAAU,mCAAoC,sBACpD,aAEW,mBAAU,oCAAqC,sBAEpEH,UAAUE,UAAYO,YACJ,kBAAXf,SACPM,UAAUE,UAAY,IACtBF,UAAUE,gBAAkBU,mBAAUC,OAAO,oCAAqC"}
\ No newline at end of file
+{"version":3,"file":"reviewing.min.js","sources":["../src/reviewing.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 * Implements reviewing functionality\n *\n * @module mod_moodleoverflow/reviewing\n * @copyright 2022 Justus Dieckmann WWU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\nimport Prefetch from 'core/prefetch';\nimport Templates from 'core/templates';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Init function.\n */\nexport function init() {\n Prefetch.prefetchTemplates(['mod_moodleoverflow/reject_post_form', 'mod_moodleoverflow/review_buttons']);\n Prefetch.prefetchStrings('mod_moodleoverflow',\n ['post_was_approved',\n 'jump_to_next_post_needing_review',\n 'there_are_no_posts_needing_review',\n 'post_was_rejected']);\n\n const root = document.getElementById('moodleoverflow-posts');\n root.onclick = async(e) => {\n const action = e.target.getAttribute('data-moodleoverflow-action');\n\n if (!action) {\n return;\n }\n\n const post = e.target.closest('*[data-moodleoverflow-postid]');\n const reviewRow = e.target.closest('.reviewrow');\n const postID = post.getAttribute('data-moodleoverflow-postid');\n\n if (action === 'approve') {\n reviewRow.innerHTML = '.';\n const nextPostURL = await Ajax.call([{\n methodname: 'mod_moodleoverflow_review_approve_post',\n args: {\n postid: postID,\n }\n }])[0];\n\n let message = await getString('post_was_approved', 'mod_moodleoverflow') + ' ';\n if (nextPostURL) {\n message += ``\n + await getString('jump_to_next_post_needing_review', 'mod_moodleoverflow')\n + \"\";\n } else {\n message += await getString('there_are_no_posts_needing_review', 'mod_moodleoverflow');\n }\n reviewRow.innerHTML = message;\n post.classList.remove(\"pendingreview\");\n } else if (action === 'reject') {\n reviewRow.innerHTML = '.';\n reviewRow.innerHTML = await Templates.render('mod_moodleoverflow/reject_post_form', {});\n } else if (action === 'reject-submit') {\n const rejectMessage = post.querySelector('textarea.reject-reason').value.toString().trim();\n reviewRow.innerHTML = '.';\n const args = {\n postid: postID,\n reason: rejectMessage ? rejectMessage : null\n };\n const nextPostURL = await Ajax.call([{\n methodname: 'mod_moodleoverflow_review_reject_post',\n args: args\n }])[0];\n\n let message = await getString('post_was_rejected', 'mod_moodleoverflow') + ' ';\n if (nextPostURL) {\n message += ``\n + await getString('jump_to_next_post_needing_review', 'mod_moodleoverflow')\n + \"\";\n } else {\n message += await getString('there_are_no_posts_needing_review', 'mod_moodleoverflow');\n }\n reviewRow.innerHTML = message;\n } else if (action === 'reject-cancel') {\n reviewRow.innerHTML = '.';\n reviewRow.innerHTML = await Templates.render('mod_moodleoverflow/review_buttons', {});\n }\n };\n}"],"names":["_interopRequireDefault","e","__esModule","default","Prefetch","prefetchTemplates","prefetchStrings","document","getElementById","onclick","async","action","target","getAttribute","post","closest","reviewRow","postID","innerHTML","nextPostURL","Ajax","call","methodname","args","postid","message","getString","get_string","classList","remove","Templates","render","rejectMessage","querySelector","value","toString","trim","reason","_ajax","_prefetch","_templates"],"mappings":"6JAwBuC,SAAAA,uBAAAC,GAAAA,OAAAA,GAAAA,EAAAC,WAAAD,EAAAE,CAAAA,QAAAF,EAAA;;;;;;;2EAMhC,WACHG,UAAQD,QAACE,kBAAkB,CAAC,sCAAuC,sCACnED,UAAAA,QAASE,gBAAgB,qBACrB,CAAC,oBACM,mCACA,oCACA,sBAEEC,SAASC,eAAe,wBAChCC,QAAUC,UACX,MAAMC,OAASV,EAAEW,OAAOC,aAAa,8BAErC,IAAKF,OACD,OAGJ,MAAMG,KAAOb,EAAEW,OAAOG,QAAQ,iCACxBC,UAAYf,EAAEW,OAAOG,QAAQ,cAC7BE,OAASH,KAAKD,aAAa,8BAEjC,GAAe,YAAXF,OAAsB,CACtBK,UAAUE,UAAY,IACtB,MAAMC,kBAAoBC,cAAKC,KAAK,CAAC,CACjCC,WAAY,yCACZC,KAAM,CACFC,OAAQP,WAEZ,GAEJ,IAAIQ,cAAgB,EAAAC,KAAAA,YAAU,oBAAqB,sBAAwB,IAEvED,SADAN,YACW,YAAYA,sBACX,EAAAO,KAASC,YAAC,mCAAoC,sBACpD,aAEW,EAAAD,KAAAA,YAAU,oCAAqC,sBAEpEV,UAAUE,UAAYO,QACtBX,KAAKc,UAAUC,OAAO,gBAC1B,MAAO,GAAe,WAAXlB,OACPK,UAAUE,UAAY,IACtBF,UAAUE,gBAAkBY,WAAS3B,QAAC4B,OAAO,sCAAuC,CAAA,QACjF,GAAe,kBAAXpB,OAA4B,CACnC,MAAMqB,cAAgBlB,KAAKmB,cAAc,0BAA0BC,MAAMC,WAAWC,OACpFpB,UAAUE,UAAY,IACtB,MAAMK,KAAO,CACTC,OAAQP,OACRoB,OAAQL,eAAgC,MAEtCb,kBAAoBC,cAAKC,KAAK,CAAC,CACjCC,WAAY,wCACZC,KAAMA,QACN,GAEJ,IAAIE,cAAgB,EAAAC,KAAAA,YAAU,oBAAqB,sBAAwB,IAEvED,SADAN,YACW,YAAYA,sBACX,EAAAO,KAASC,YAAC,mCAAoC,sBACpD,aAEW,EAAAD,KAAAA,YAAU,oCAAqC,sBAEpEV,UAAUE,UAAYO,OAC1B,KAAsB,kBAAXd,SACPK,UAAUE,UAAY,IACtBF,UAAUE,gBAAkBY,WAAS3B,QAAC4B,OAAO,oCAAqC,CAAA,IAG9F,EA5EAO,MAAAtC,uBAAAsC,OACAC,UAAAvC,uBAAAuC,WACAC,WAAAxC,uBAAAwC,WA0EC"}
\ No newline at end of file
diff --git a/amd/build/warnmodechange.min.js b/amd/build/warnmodechange.min.js
index 5042ba704b..605c6f767f 100644
--- a/amd/build/warnmodechange.min.js
+++ b/amd/build/warnmodechange.min.js
@@ -1,10 +1,10 @@
-define("mod_moodleoverflow/warnmodechange",["exports","core/str","core/notification","core/prefetch"],(function(_exports,_str,_notification,_prefetch){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+define("mod_moodleoverflow/warnmodechange",["exports","core/str","core/notification","core/prefetch"],function(_exports,_str,_notification,_prefetch){function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}
/**
* Warns on changing the subscription mode.
*
* @module mod_moodleoverflow/warnmodechange
* @copyright 2022 Justus Dieckmann WWU
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(previousSetting){_prefetch.default.prefetchStrings("mod_moodleoverflow",["switchtooptional","switchtoauto"]),_prefetch.default.prefetchStrings("moodle",["confirm","cancel"]);const form=document.querySelector("form.mform"),select=document.getElementById("id_forcesubscribe");form.onsubmit=async e=>{const value=select.selectedOptions[0].value;value!=previousSetting&&1!=value&&3!=value&&(e.preventDefault(),await _notification.default.confirm(await(0,_str.get_string)("confirm"),await(0,_str.get_string)(0==value?"switchtooptional":"switchtoauto","mod_moodleoverflow"),await(0,_str.get_string)("confirm"),await(0,_str.get_string)("cancel"),(()=>{form.onsubmit=void 0,form.requestSubmit(e.submitter)}),void 0))}},_notification=_interopRequireDefault(_notification),_prefetch=_interopRequireDefault(_prefetch)}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(previousSetting){_prefetch.default.prefetchStrings("mod_moodleoverflow",["switchtooptional","switchtoauto"]),_prefetch.default.prefetchStrings("moodle",["confirm","cancel"]);const form=document.querySelector("form.mform"),select=document.getElementById("id_forcesubscribe");form.onsubmit=async e=>{const value=select.selectedOptions[0].value;value!=previousSetting&&1!=value&&3!=value&&(e.preventDefault(),await _notification.default.confirm(await(0,_str.get_string)("confirm"),await(0,_str.get_string)(0==value?"switchtooptional":"switchtoauto","mod_moodleoverflow"),await(0,_str.get_string)("confirm"),await(0,_str.get_string)("cancel"),()=>{form.onsubmit=void 0,form.requestSubmit(e.submitter)},void 0))}},_notification=_interopRequireDefault(_notification),_prefetch=_interopRequireDefault(_prefetch)});
//# sourceMappingURL=warnmodechange.min.js.map
\ No newline at end of file
diff --git a/amd/build/warnmodechange.min.js.map b/amd/build/warnmodechange.min.js.map
index 70683e5540..1d84d0f41f 100644
--- a/amd/build/warnmodechange.min.js.map
+++ b/amd/build/warnmodechange.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"warnmodechange.min.js","sources":["../src/warnmodechange.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 * Warns on changing the subscription mode.\n *\n * @module mod_moodleoverflow/warnmodechange\n * @copyright 2022 Justus Dieckmann WWU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {get_string as getString} from 'core/str';\nimport Notification from 'core/notification';\nimport Prefetch from 'core/prefetch';\n\n/**\n * Init function.\n * @param {string} previousSetting\n */\nexport function init(previousSetting) {\n Prefetch.prefetchStrings('mod_moodleoverflow', ['switchtooptional', 'switchtoauto']);\n Prefetch.prefetchStrings('moodle', ['confirm', 'cancel']);\n const form = document.querySelector('form.mform');\n const select = document.getElementById('id_forcesubscribe');\n form.onsubmit = async(e) => {\n const value = select.selectedOptions[0].value;\n if (value == previousSetting || value == 1 || value == 3) {\n return;\n }\n e.preventDefault();\n await Notification.confirm(\n await getString('confirm'),\n await getString(value == 0 ? 'switchtooptional' : 'switchtoauto', 'mod_moodleoverflow'),\n await getString('confirm'),\n await getString('cancel'),\n () => {\n // Prevent this listener from preventing the event again.\n form.onsubmit = undefined;\n form.requestSubmit(e.submitter);\n }, undefined);\n };\n}"],"names":["previousSetting","prefetchStrings","form","document","querySelector","select","getElementById","onsubmit","async","value","selectedOptions","e","preventDefault","Notification","confirm","undefined","requestSubmit","submitter"],"mappings":";;;;;;;oFA8BqBA,mCACRC,gBAAgB,qBAAsB,CAAC,mBAAoB,mCAC3DA,gBAAgB,SAAU,CAAC,UAAW,iBACzCC,KAAOC,SAASC,cAAc,cAC9BC,OAASF,SAASG,eAAe,qBACvCJ,KAAKK,SAAWC,MAAAA,UACNC,MAAQJ,OAAOK,gBAAgB,GAAGD,MACpCA,OAAST,iBAA4B,GAATS,OAAuB,GAATA,QAG9CE,EAAEC,uBACIC,sBAAaC,cACT,mBAAU,iBACV,mBAAmB,GAATL,MAAa,mBAAqB,eAAgB,4BAC5D,mBAAU,iBACV,mBAAU,WAChB,KAEIP,KAAKK,cAAWQ,EAChBb,KAAKc,cAAcL,EAAEM,kBACtBF"}
\ No newline at end of file
+{"version":3,"file":"warnmodechange.min.js","sources":["../src/warnmodechange.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 * Warns on changing the subscription mode.\n *\n * @module mod_moodleoverflow/warnmodechange\n * @copyright 2022 Justus Dieckmann WWU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {get_string as getString} from 'core/str';\nimport Notification from 'core/notification';\nimport Prefetch from 'core/prefetch';\n\n/**\n * Init function.\n * @param {string} previousSetting\n */\nexport function init(previousSetting) {\n Prefetch.prefetchStrings('mod_moodleoverflow', ['switchtooptional', 'switchtoauto']);\n Prefetch.prefetchStrings('moodle', ['confirm', 'cancel']);\n const form = document.querySelector('form.mform');\n const select = document.getElementById('id_forcesubscribe');\n form.onsubmit = async(e) => {\n const value = select.selectedOptions[0].value;\n if (value == previousSetting || value == 1 || value == 3) {\n return;\n }\n\n e.preventDefault();\n await Notification.confirm(\n await getString('confirm'),\n await getString(value == 0 ? 'switchtooptional' : 'switchtoauto', 'mod_moodleoverflow'),\n await getString('confirm'),\n await getString('cancel'),\n () => {\n // Prevent this listener from preventing the event again.\n form.onsubmit = undefined;\n form.requestSubmit(e.submitter);\n }, undefined);\n };\n}"],"names":["_interopRequireDefault","e","__esModule","default","previousSetting","Prefetch","prefetchStrings","form","document","querySelector","select","getElementById","onsubmit","async","value","selectedOptions","preventDefault","Notification","confirm","getString","undefined","requestSubmit","submitter","_notification","_prefetch"],"mappings":"sJAwBqC,SAAAA,uBAAAC,GAAAA,OAAAA,GAAAA,EAAAC,WAAAD,EAAAE,CAAAA,QAAAF,EAAA;;;;;;;2EAM9B,SAAcG,iBACjBC,UAAQF,QAACG,gBAAgB,qBAAsB,CAAC,mBAAoB,iBACpED,UAAQF,QAACG,gBAAgB,SAAU,CAAC,UAAW,WAC/C,MAAMC,KAAOC,SAASC,cAAc,cAC9BC,OAASF,SAASG,eAAe,qBACvCJ,KAAKK,SAAWC,UACZ,MAAMC,MAAQJ,OAAOK,gBAAgB,GAAGD,MACpCA,OAASV,iBAA4B,GAATU,OAAuB,GAATA,QAI9Cb,EAAEe,uBACIC,sBAAaC,cACT,EAAAC,iBAAU,iBACV,EAAAA,KAAAA,YAAmB,GAATL,MAAa,mBAAqB,eAAgB,4BAC5D,EAAAK,iBAAU,iBACV,EAAAA,KAAAA,YAAU,UAChB,KAEIZ,KAAKK,cAAWQ,EAChBb,KAAKc,cAAcpB,EAAEqB,iBACtBF,IAEf,EA9BAG,cAAAvB,uBAAAuB,eACAC,UAAAxB,uBAAAwB,UA6BC"}
\ No newline at end of file
diff --git a/amd/src/activityhelp.js b/amd/src/activityhelp.js
index 601b6ad243..58427b30b4 100644
--- a/amd/src/activityhelp.js
+++ b/amd/src/activityhelp.js
@@ -21,8 +21,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-
-
const Selectors = {
actions: {
showHelpIcon: '[data-action="showhelpicon"]',
@@ -30,7 +28,7 @@ const Selectors = {
};
/**
- * Function that shows the help string.
+ * Function that shows the help icon string.
*/
export const init = () => {
document.addEventListener('click', event => {
diff --git a/amd/src/rating.js b/amd/src/rating.js
index 0f44f3ba77..259e2bc8c2 100644
--- a/amd/src/rating.js
+++ b/amd/src/rating.js
@@ -48,15 +48,19 @@ async function sendVote(postid, rating, userid) {
ratingid: rating
}
}])[0];
+
root.querySelectorAll(`[data-moodleoverflow-userreputation="${userid}"]`).forEach((i) => {
i.textContent = response.raterreputation;
});
+
root.querySelectorAll(`[data-moodleoverflow-userreputation="${response.ownerid}"]`).forEach((i) => {
i.textContent = response.ownerreputation;
});
+
root.querySelectorAll(`[data-moodleoverflow-postreputation="${postid}"]`).forEach((i) => {
i.textContent = response.postrating;
});
+
return response;
}
@@ -149,7 +153,7 @@ export function init(userid, allowmultiplemarks) {
/**
* Function to change the String of the post data-action button.
- * Only used if mulitplemarks are allowed.
+ * Only used if multiplemarks are allowed.
* @param {string} htmlclass the class where the String is being updated
* @param {string} action helpful or solved mark
*/
diff --git a/amd/src/reviewing.js b/amd/src/reviewing.js
index 0525609f59..72213cbb0d 100644
--- a/amd/src/reviewing.js
+++ b/amd/src/reviewing.js
@@ -31,7 +31,10 @@ import {get_string as getString} from 'core/str';
export function init() {
Prefetch.prefetchTemplates(['mod_moodleoverflow/reject_post_form', 'mod_moodleoverflow/review_buttons']);
Prefetch.prefetchStrings('mod_moodleoverflow',
- ['post_was_approved', 'jump_to_next_post_needing_review', 'there_are_no_posts_needing_review', 'post_was_rejected']);
+ ['post_was_approved',
+ 'jump_to_next_post_needing_review',
+ 'there_are_no_posts_needing_review',
+ 'post_was_rejected']);
const root = document.getElementById('moodleoverflow-posts');
root.onclick = async(e) => {
diff --git a/amd/src/warnmodechange.js b/amd/src/warnmodechange.js
index 4d59b2dc2e..afb4cee3e9 100644
--- a/amd/src/warnmodechange.js
+++ b/amd/src/warnmodechange.js
@@ -38,6 +38,7 @@ export function init(previousSetting) {
if (value == previousSetting || value == 1 || value == 3) {
return;
}
+
e.preventDefault();
await Notification.confirm(
await getString('confirm'),
diff --git a/backup/moodle2/backup_moodleoverflow_stepslib.php b/backup/moodle2/backup_moodleoverflow_stepslib.php
index aa56787799..391f6754ef 100644
--- a/backup/moodle2/backup_moodleoverflow_stepslib.php
+++ b/backup/moodle2/backup_moodleoverflow_stepslib.php
@@ -39,14 +39,16 @@ class backup_moodleoverflow_activity_structure_step extends backup_activity_stru
* @return backup_nested_element
*/
protected function define_structure() {
+
// To know if we are including userinfo.
$userinfo = $this->get_setting_value('userinfo');
// Define the root element describing the moodleoverflow instance.
$moodleoverflow = new backup_nested_element('moodleoverflow', ['id'], [
- 'name', 'intro', 'introformat', 'maxbytes', 'maxattachments', 'timecreated', 'timemodified', 'forcesubscribe',
- 'trackingtype', 'ratingpreference', 'coursewidereputation', 'allowrating', 'allowreputation', 'allownegativereputation',
- 'grademaxgrade', 'gradescalefactor', 'gradecat', 'anonymous', 'allowmultiplemarks', ]);
+ 'name', 'intro', 'introformat', 'maxbytes', 'maxattachments', 'timecreated', 'timemodified',
+ 'forcesubscribe', 'trackingtype', 'ratingpreference', 'coursewidereputation', 'allowrating',
+ 'allowreputation', 'allownegativereputation', 'grademaxgrade', 'gradescalefactor', 'gradecat',
+ 'anonymous', 'allowmultiplemarks', 'la_starttime', 'la_endtime', ]);
// Define each element separated.
$discussions = new backup_nested_element('discussions');
@@ -54,20 +56,24 @@ protected function define_structure() {
'name', 'firstpost', 'userid', 'timestart', 'timemodified', 'usermodified', ]);
$posts = new backup_nested_element('posts');
- $post = new backup_nested_element('post', ['id'], ['parent', 'userid', 'created', 'modified', 'message',
- 'messageformat', 'attachment', 'mailed', 'reviewed', 'timereviewed', ]);
+ $post = new backup_nested_element('post', ['id'], [
+ 'parent', 'userid', 'created', 'modified',
+ 'message', 'messageformat', 'attachment', 'mailed', 'reviewed', 'timereviewed', ]);
$ratings = new backup_nested_element('ratings');
- $rating = new backup_nested_element('rating', ['id'], ['userid', 'rating', 'firstrated', 'lastchanged']);
+ $rating = new backup_nested_element('rating', ['id'], [
+ 'userid', 'rating', 'firstrated', 'lastchanged', ]);
$discussionsubs = new backup_nested_element('discuss_subs');
- $discussionsub = new backup_nested_element('discuss_sub', ['id'], ['userid', 'preference']);
+ $discussionsub = new backup_nested_element('discuss_sub', ['id'], [
+ 'userid', 'preference', ]);
$subscriptions = new backup_nested_element('subscriptions');
$subscription = new backup_nested_element('subscription', ['id'], ['userid']);
$readposts = new backup_nested_element('readposts');
- $read = new backup_nested_element('read', ['id'], ['userid', 'discussionid', 'postid', 'firstread', 'lastread']);
+ $read = new backup_nested_element('read', ['id'], [
+ 'userid', 'discussionid', 'postid', 'firstread', 'lastread', ]);
$grades = new backup_nested_element('grades');
$grade = new backup_nested_element('grade', ['id'], ['userid', 'grade']);
@@ -94,9 +100,6 @@ protected function define_structure() {
$moodleoverflow->add_child($readposts);
$readposts->add_child($read);
- $moodleoverflow->add_child($grades);
- $grades->add_child($grade);
-
$moodleoverflow->add_child($tracking);
$tracking->add_child($track);
diff --git a/classes/discussion/discussion.php b/classes/discussion/discussion.php
new file mode 100644
index 0000000000..68b2d7a03d
--- /dev/null
+++ b/classes/discussion/discussion.php
@@ -0,0 +1,624 @@
+.
+
+/**
+ * Class for working with posts
+ *
+ * @package mod_moodleoverflow
+ * @copyright 2023 Tamaro Walter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_moodleoverflow\discussion;
+
+
+// Import namespace from the locallib, needs a check later which namespaces are really needed.
+use mod_moodleoverflow\anonymous;
+
+// Important namespaces.
+use mod_moodleoverflow\readtracking;
+use mod_moodleoverflow\review;
+use mod_moodleoverflow\post\post;
+use mod_moodleoverflow\capabilities;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/moodleoverflow/locallib.php');
+
+/**
+ * Class that represents a discussion. A discussion administrates the posts and has one parent post, that started the discussion.
+ *
+ * @package mod_moodleoverflow
+ * @copyright 2023 Tamaro Walter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ *
+ * Please be careful with functions that delete, add or edit posts and discussions.
+ * Security checks for these functions were done in the post_control class and these functions should only be accessed that way.
+ * Accessing these functions directly without the checks from the post control could lead to serious errors.
+ */
+class discussion {
+
+ /** @var int The discussion ID */
+ private $id;
+
+ /** @var int The course ID where the discussion is located */
+ private $course;
+
+ /** @var int The moodleoverflow ID where the discussion is located*/
+ private $moodleoverflow;
+
+ /** @var string The title of the discussion, the titel of the parent post*/
+ public $name;
+
+ /** @var int The id of the parent/first post*/
+ private $firstpost;
+
+ /** @var int The user ID who started the discussion */
+ private $userid;
+
+ /** @var int Unix-timestamp of modification */
+ public $timemodified;
+
+ /** @var int Unix-timestamp of discussion creation */
+ public $timestart;
+
+ /** @var int the user ID who modified the discussion */
+ public $usermodified;
+
+ // Not Database-related attributes.
+
+ /** @var array an Array of posts that belong to this discussion */
+ public $posts;
+
+ /** @var bool a variable for checking if this instance has all its posts */
+ public $postsbuild;
+
+ /** @var object The moodleoverflow object where the discussion is located */
+ public $moodleoverflowobject;
+
+ /** @var object The course module object */
+ public $cmobject;
+
+ // Constructors and other builders.
+
+ /**
+ * Constructor to build a new discussion.
+ * @param int $id The Discussion ID.
+ * @param int $course The course ID.
+ * @param int $moodleoverflow The moodleoverflow ID.
+ * @param char $name Discussion Title.
+ * @param int $firstpost .
+ * @param int $userid The course ID.
+ * @param int $timemodified The course ID.
+ * @param int $timestart The course ID.
+ * @param int $usermodified The course ID.
+ */
+ public function __construct($id, $course, $moodleoverflow, $name, $firstpost,
+ $userid, $timemodified, $timestart, $usermodified) {
+ $this->id = $id;
+ $this->course = $course;
+ $this->moodleoverflow = $moodleoverflow;
+ $this->name = $name;
+ $this->firstpost = $firstpost;
+ $this->userid = $userid;
+ $this->timemodified = $timemodified;
+ $this->timestart = $timestart;
+ $this->usermodified = $usermodified;
+ $this->posts = [];
+ $this->postsbuild = false;
+ }
+
+ /**
+ * Builds a Discussion from a DB record.
+ *
+ * @param object $record Data object.
+ * @return object discussion instance
+ */
+ public static function from_record($record) {
+ $id = null;
+ if (object_property_exists($record, 'id') && $record->id) {
+ $id = $record->id;
+ }
+
+ $course = 0;
+ if (object_property_exists($record, 'course') && $record->course) {
+ $course = $record->course;
+ }
+
+ $moodleoverflow = 0;
+ if (object_property_exists($record, 'moodleoverflow') && $record->moodleoverflow) {
+ $moodleoverflow = $record->moodleoverflow;
+ }
+
+ $name = '';
+ if (object_property_exists($record, 'name') && $record->name) {
+ $name = $record->name;
+ }
+
+ $firstpost = 0;
+ if (object_property_exists($record, 'firstpost') && $record->firstpost) {
+ $firstpost = $record->firstpost;
+ }
+
+ $userid = 0;
+ if (object_property_exists($record, 'userid') && $record->userid) {
+ $userid = $record->userid;
+ }
+
+ $timemodified = 0;
+ if (object_property_exists($record, 'timemodified') && $record->timemodified) {
+ $timemodified = $record->timemodified;
+ }
+
+ $timestart = 0;
+ if (object_property_exists($record, 'timestart') && $record->timestart) {
+ $timestart = $record->timestart;
+ }
+
+ $usermodified = 0;
+ if (object_property_exists($record, 'usermodified') && $record->usermodified) {
+ $usermodified = $record->usermodified;
+ }
+
+ $instance = new self($id, $course, $moodleoverflow, $name, $firstpost, $userid, $timemodified, $timestart, $usermodified);
+
+ // Get all the posts so that the instance can work with it.
+ $instance->moodleoverflow_get_discussion_posts();
+
+ return $instance;
+ }
+
+ /**
+ * Function to build a new discussion without specifying the Discussion ID.
+ * @param int $course The course ID.
+ * @param int $moodleoverflow The moodleoverflow ID.
+ * @param char $name Discussion Title.
+ * @param int $firstpost .
+ * @param int $userid The course ID.
+ * @param int $timemodified The course ID.
+ * @param int $timestart The course ID.
+ * @param int $usermodified The course ID.
+ *
+ * @return object discussion object without id.
+ */
+ public static function construct_without_id($course, $moodleoverflow, $name, $firstpost,
+ $userid, $timemodified, $timestart, $usermodified) {
+ $id = null;
+ $instance = new self($id, $course, $moodleoverflow, $name, $firstpost, $userid, $timemodified, $timestart, $usermodified);
+ return $instance;
+ }
+
+ // Discussion Functions.
+
+ /**
+ * Adds a new Discussion with a post.
+ *
+ * @param object $prepost The prepost object from the post_control. Has information about the post and other important stuff.
+ */
+ public function moodleoverflow_add_discussion($prepost) {
+ global $DB;
+
+ // Add the discussion to the Database.
+ $this->id = $DB->insert_record('moodleoverflow_discussions', $this->build_db_object());
+
+ // Create the first/parent post for the new discussion and add it do the DB.
+ $post = post::construct_without_id($this->id, 0, $prepost->userid, $prepost->timenow, $prepost->timenow, $prepost->message,
+ $prepost->messageformat, "", 0, $prepost->reviewed, null, $prepost->formattachments);
+ // Add it to the DB and save the id of the first/parent post.
+ $this->firstpost = $post->moodleoverflow_add_new_post();
+
+ // Save the id of the first/parent post in the DB.
+ $DB->set_field('moodleoverflow_discussions', 'firstpost', $this->firstpost, ['id' => $this->id]);
+
+ // Add the parent post to the $posts array.
+ $this->posts[$this->firstpost] = $post;
+ $this->postsbuild = true;
+
+ // Trigger event.
+ $params = [
+ 'context' => $prepost->modulecontext,
+ 'objectid' => $this->id,
+ ];
+ // LEARNWEB-TODO: check if the event functions.
+ $event = \mod_moodleoverflow\event\discussion_viewed::create($params);
+ $event->trigger();
+
+ // Return the id of the discussion.
+ return $this->id;
+ }
+
+ /**
+ * Delete a discussion with all of it's posts
+ * @param object $prepost Information about the post from the post_control
+ * @return bool Wether deletion was successful of not
+ */
+ public function moodleoverflow_delete_discussion($prepost) {
+ global $DB;
+ $this->existence_check();
+ $this->posts_check();
+
+ // Delete a discussion with all of it's posts.
+ // In case something does not work we throw the error as it should be known that something went ... terribly wrong.
+ // All DB transactions are rolled back.
+ try {
+ $transaction = $DB->start_delegated_transaction();
+
+ // Delete every post of this discussion.
+ foreach ($this->posts as $post) {
+ $post->moodleoverflow_delete_post(false);
+ }
+
+ // Delete the read-records for the discussion.
+ readtracking::moodleoverflow_delete_read_records(-1, -1, $this->id);
+
+ // Remove the subscriptions for the discussion.
+ $DB->delete_records('moodleoverflow_discuss_subs', ['discussion' => $this->id]);
+
+ // Delete the discussion from the database.
+ $DB->delete_records('moodleoverflow_discussions', ['id' => $this->id]);
+
+ // Trigger the discussion deleted event.
+ $params = [
+ 'objectid' => $this->id,
+ 'context' => $prepost->modulecontext,
+ ];
+
+ $event = \mod_moodleoverflow\event\discussion_deleted::create($params);
+ $event->trigger();
+
+ // Set the id of this instance to null, so that working with it is not possible anymore.
+ $this->id = null;
+
+ // The discussion has been deleted.
+ $transaction->allow_commit();
+ return true;
+
+ } catch (Exception $e) {
+ $transaction->rollback($e);
+ }
+
+ // Deleting the discussion has failed.
+ return false;
+ }
+
+ /**
+ * Adds a new post to this discussion and the DB.
+ *
+ * @param object $prepost The prepost object from the post_control. Has Information about the post and other important stuff.
+ */
+ public function moodleoverflow_add_post_to_discussion($prepost) {
+ global $DB;
+ $this->existence_check();
+ $this->posts_check();
+
+ // Create the post that will be added to the new discussion.
+ $post = post::construct_without_id($this->id, $prepost->parentid, $prepost->userid, $prepost->timenow, $prepost->timenow,
+ $prepost->message, $prepost->messageformat, "", 0, $prepost->reviewed, null,
+ $prepost->formattachments);
+ // Add the post to the DB.
+ $postid = $post->moodleoverflow_add_new_post();
+
+ // Add the post to the $posts array and update the timemodified in the DB.
+ $this->posts[$postid] = $post;
+ $this->timemodified = $prepost->timenow;
+ $this->usermodified = $prepost->userid;
+ $DB->update_record('moodleoverflow_discussions', $this->build_db_object());
+
+ // Return the id of the added post.
+ return $postid;
+ }
+
+ /**
+ * Deletes a post that is in this discussion from the DB.
+ * @param object $prepost The prepost object from the post_control. Has Information about the post and other important stuff.
+ * @return bool Wether the deletion was possible
+ * @throws moodle_exception if post is not in this discussion or something failed.
+ */
+ public function moodleoverflow_delete_post_from_discussion($prepost) {
+ $this->existence_check();
+ $this->posts_check();
+
+ // Check if the posts exists in this discussion.
+ $this->post_exists_check($prepost->postid);
+
+ // Access the post and delete it.
+ $post = $this->posts[$prepost->postid];
+ if (!$post->moodleoverflow_delete_post($prepost->deletechildren)) {
+ // Deletion failed.
+ return false;
+ }
+
+ // Check for the new last post of the discussion.
+ $this->moodleoverflow_discussion_adapt_to_last_post();
+
+ // Delete the post from the post array.
+ unset($this->posts[$prepost->postid]);
+
+ return true;
+ }
+
+ /**
+ * Edits the message of a post from this discussion.
+ * @param object $prepost The prepost object from the post_control. Has Information about the post and other important stuff.
+ */
+ public function moodleoverflow_edit_post_from_discussion($prepost) {
+ global $DB;
+ $this->existence_check();
+ $this->posts_check();
+
+ // Check if the posts exists in this discussion.
+ $this->post_exists_check($prepost->postid);
+
+ // Access the post.
+ $post = $this->posts[$prepost->postid];
+
+ // If the post is the firstpost, then update the name of this discussion and the post. If not, only update the post.
+ if ($prepost->postid == array_key_first($this->posts)) {
+ $this->name = $prepost->subject;
+ $this->usermodified = $prepost->userid;
+ $this->timemodified = $prepost->timenow;
+ $DB->update_record('moodleoverflow_discussions', $this->build_db_object());
+ }
+ $post->moodleoverflow_edit_post($prepost->timenow, $prepost->message, $prepost->messageformat, $prepost->formattachments);
+
+ // The post has been edited successfully.
+ return true;
+ }
+
+ /**
+ * This Function checks, what the last added or edited post is. If it changed by a delete function,
+ * the timemodified and the usermodified need to be adapted to the last added or edited post.
+ *
+ * @return bool true if the DB needed to be adapted. false if it didn't change.
+ */
+ public function moodleoverflow_discussion_adapt_to_last_post() {
+ global $DB;
+ $this->existence_check();
+
+ // Find the last reviewed post of the discussion (even if the user has review capability, because it's written to DB).
+ $sql = 'SELECT *
+ FROM {moodleoverflow_posts}
+ WHERE discussion = ' . $this->id .
+ ' AND reviewed = 1
+ AND modified = (SELECT MAX(modified) as modified
+ FROM {moodleoverflow_posts}
+ WHERE discussion = ' . $this->id . ');';
+ $record = $DB->get_record_sql($sql);
+ $lastpost = post::from_record($record);
+
+ // Check if the last post changed. If it changed, then update the DB-record of this discussion.
+ if ($lastpost->modified != $this->timemodified || $lastpost->get_userid() != $this->usermodified) {
+ $this->timemodified = $lastpost->modified;
+ $this->usermodified = $lastpost->get_userid();
+ $DB->update_record('moodleoverflow_discussions', $this->build_db_object());
+
+ // Return that the discussion needed an update.
+ return true;
+ }
+
+ // Return that the discussion didn't need an update.
+ return false;
+ }
+
+ // Getter.
+
+ /**
+ * Getter for the post ID
+ * @return int $this->id The post ID.
+ */
+ public function get_id() {
+ $this->existence_check();
+ return $this->id;
+ }
+
+ /**
+ * Getter for the courseid
+ * @return int $this->course The ID of the course where the discussion is located.
+ */
+ public function get_courseid() {
+ $this->existence_check();
+ return $this->course;
+ }
+
+ /**
+ * Getter for the moodleoverflowid
+ * @return int $this->moodleoverflow The ID of the moodleoverflow where the discussion is located.
+ */
+ public function get_moodleoverflowid() {
+ $this->existence_check();
+ return $this->moodleoverflow;
+ }
+
+ /**
+ * Getter for the firstpostid
+ * @return int $this->firstpost The ID of the first post.
+ */
+ public function get_firstpostid() {
+ $this->existence_check();
+ return $this->firstpost;
+ }
+
+ /**
+ * Getter for the userid
+ * @return int $this->userid The ID of the user who wrote the first post.
+ */
+ public function get_userid() {
+ $this->existence_check();
+ return $this->userid;
+ }
+
+ /**
+ * Returns the ratings from this discussion.
+ * @return array of votings
+ */
+ public function moodleoverflow_get_discussion_ratings() {
+ $this->existence_check();
+ $this->posts_check();
+
+ $discussionratings = \mod_moodleoverflow\ratings::moodleoverflow_get_ratings_by_discussion($this->id);
+ return $discussionratings;
+ }
+
+ /**
+ * Get all posts from this Discussion.
+ * The first/parent post is on the first position in the array.
+ *
+ * @return array $posts Array ob posts objects
+ */
+ public function moodleoverflow_get_discussion_posts() {
+ global $DB;
+ $this->existence_check();
+
+ // Check if the posts array are build yet. If not, build it.
+ if (!$this->postsbuild) {
+ // Get the posts from the DB. Get the parent post first.
+ $firstpostsql = 'SELECT * FROM {moodleoverflow_posts} posts
+ WHERE discussion = ' . $this->id . ' AND parent = 0;';
+ $otherpostssql = 'SELECT * FROM {moodleoverflow_posts} posts
+ WHERE discussion = ' . $this->id . ' AND parent != 0;';
+ $firstpostrecord = $DB->get_record_sql($firstpostsql);
+ $otherpostsrecord = $DB->get_records_sql($otherpostssql);
+
+ // Add the first/parent post to the array, then add the other posts.
+ $firstpost = post::from_record($firstpostrecord);
+ $this->posts[$firstpost->get_id()] = $firstpost;
+
+ foreach ($otherpostsrecord as $postrecord) {
+ $post = post::from_record($postrecord);
+ $this->posts[$post->get_id()] = $post;
+ }
+
+ // Now the posts are built.
+ $this->postsbuild = true;
+ }
+
+ // Return the posts array.
+ return $this->posts;
+ }
+
+
+ /**
+ * Returns the moodleoverflowobject
+ *
+ * @return object $moodleoverflowobject
+ */
+ public function get_moodleoverflow() {
+ global $DB;
+ $this->existence_check();
+
+ if (empty($this->moodleoverflowobject)) {
+ $this->moodleoverflowobject = $DB->get_records('moodleoverflow', ['id' => $this->moodleoverflow]);
+ }
+
+ return $this->moodleoverflowobject;
+ }
+
+ /**
+ * Returns the coursemodule
+ *
+ * @return object $cmobject
+ */
+ public function get_coursemodule() {
+ global $DB;
+ $this->existence_check();
+
+ if (empty($this->cmobject)) {
+ if (!$this->cmobject = $DB->get_coursemodule_from_instance('moodleoverflow', $this->get_moodleoverflow()->id,
+ $this->get_moodleoverflow()->course)) {
+ throw new \moodle_exception('invalidcoursemodule');
+ }
+ }
+
+ return $this->cmobject;
+ }
+
+ /**
+ * This getter works as an help function in case another file/function needs the db-object of this instance (as the function
+ * is not adapted/refactored to the new way of working with discussion).
+ * @return object
+ */
+ public function get_db_object() {
+ $this->existence_check();
+ return $this->build_db_object();
+ }
+
+ // Helper functions.
+
+ /**
+ * Builds an object from this instance that has only DB-relevant attributes.
+ * As this is an private function, it doesn't need an existence check.
+ * @return object $dbobject
+ */
+ private function build_db_object() {
+ $dbobject = new \stdClass();
+ $dbobject->id = $this->id;
+ $dbobject->course = $this->course;
+ $dbobject->moodleoverflow = $this->moodleoverflow;
+ $dbobject->name = $this->name;
+ $dbobject->firstpost = $this->firstpost;
+ $dbobject->userid = $this->userid;
+ $dbobject->timemodified = $this->timemodified;
+ $dbobject->timestart = $this->timestart;
+ $dbobject->usermodified = $this->usermodified;
+
+ return $dbobject;
+ }
+
+ // Security.
+
+ /**
+ * Makes sure that the instance exists in the database. Every function in this class requires this check
+ * (except the function that adds the discussion to the database)
+ *
+ * @return true
+ * @throws moodle_exception
+ */
+ private function existence_check() {
+ if (empty($this->id) || $this->id == false || $this->id == null) {
+ throw new \moodle_exception('noexistingdiscussion', 'moodleoverflow');
+ }
+ return true;
+ }
+
+ /**
+ * Makes sure that the instance knows all of its posts (That all posts of the db are in the local array).
+ * Not all functions need this check.
+ * @return true
+ * @throws moodle_exception
+ */
+ private function posts_check() {
+ if (!$this->postsbuild) {
+ throw new \moodle_exception('notallpostsavailable', 'moodleoverflow');
+ }
+ return true;
+ }
+
+ /**
+ * Check, if certain posts really exists in this discussion.
+ *
+ * @param int $postid The ID of the post that is being checked.
+ * @return true
+ * @throws moodle_exception;
+ */
+ private function post_exists_check($postid) {
+ if (!$this->posts[$postid]) {
+ throw new \moodle_exception('postnotpartofdiscussion', 'moodleoverflow');
+ }
+
+ return true;
+ }
+}
diff --git a/classes/output/helpicon.php b/classes/output/helpicon.php
new file mode 100644
index 0000000000..b1867f850f
--- /dev/null
+++ b/classes/output/helpicon.php
@@ -0,0 +1,72 @@
+.
+
+/**
+ * Use of the Helpicon from Moodle core.
+ * @package mod_moodleoverflow
+ * @copyright 2023 Tamaro Walter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_moodleoverflow\output;
+
+/**
+ * Builds a Helpicon, that shows a String when hovering over it.
+ * @package mod_moodleoverflow
+ * @copyright 2023 Tamaro Walter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helpicon {
+
+ /** @var object The Helpicon*/
+ private $helpobject;
+
+ /**
+ * Builds a Helpicon and stores it in helpobject.
+ *
+ * @param string $htmlclass The classname in which the icon will be.
+ * @param string $content A string that shows the information that the icon has.
+ */
+ public function __construct($htmlclass, $content) {
+ global $CFG;
+ $iconurl = $CFG->wwwroot . '/pix/a/help.svg';
+ $iconstyle = ['style' =>
+ 'max-width: 20px; max-height: 20px; margin: 0; padding: 0; box-sizing: content-box; margin-right: .5rem;'];
+ $icon = \html_writer::img($iconurl, $content, $iconstyle);
+
+ $class = $htmlclass;
+ $iconattributes = ['role' => 'button',
+ 'style' => 'display: inline;',
+ 'data-container' => 'body',
+ 'data-toggle' => 'popover',
+ 'data-placement' => 'right',
+ 'data-action' => 'showhelpicon',
+ 'data-html' => 'true',
+ 'data-trigger' => 'focus',
+ 'tabindex' => '0',
+ 'data-content' => '
' . $content . '
', ];
+ $this->helpobject = \html_writer::span($icon, $class, $iconattributes);
+ }
+
+ /**
+ * Returns the Helpicon, so that it can be used.
+ *
+ * @return object The Helpicon
+ */
+ public function get_helpicon() {
+ return $this->helpobject;
+ }
+}
diff --git a/classes/output/moodleoverflow_email.php b/classes/output/moodleoverflow_email.php
index 0dcd61b8bc..27865bb49c 100644
--- a/classes/output/moodleoverflow_email.php
+++ b/classes/output/moodleoverflow_email.php
@@ -188,7 +188,7 @@ protected function export_for_template_text(\mod_moodleoverflow_renderer $render
*
* @param \mod_moodleoverflow_renderer $renderer The render to be used for formatting the message and attachments
*
- * @return stdClass Data ready for use in a mustache template
+ * @return array Data ready for use in a mustache template
*/
protected function export_for_template_html(\mod_moodleoverflow_renderer $renderer) {
return [
@@ -358,7 +358,7 @@ public function get_discussionname() {
* @return string
*/
public function get_author_fullname() {
- if (anonymous::is_post_anonymous($this->discussion, $this->moodleoverflow, $this->author->id)) {
+ if ($this->author->anonymous) {
return get_string('privacy:anonym_user_name', 'mod_moodleoverflow');
} else {
return fullname($this->author, $this->viewfullnames);
@@ -521,7 +521,7 @@ public function get_moodleoverflowviewlink() {
* @return string
*/
public function get_authorlink() {
- if (anonymous::is_post_anonymous($this->discussion, $this->moodleoverflow, $this->author->id)) {
+ if ($this->author->anonymous) {
return null;
}
@@ -542,7 +542,7 @@ public function get_authorlink() {
*/
public function get_author_picture() {
global $OUTPUT;
- if (anonymous::is_post_anonymous($this->discussion, $this->moodleoverflow, $this->author->id)) {
+ if ($this->author->anonymous) {
return '';
}
@@ -555,7 +555,7 @@ public function get_author_picture() {
* @return string
*/
public function get_group_picture() {
- if (anonymous::is_post_anonymous($this->discussion, $this->moodleoverflow, $this->author->id)) {
+ if ($this->author->anonymous) {
return '';
}
diff --git a/classes/post/post.php b/classes/post/post.php
new file mode 100644
index 0000000000..b5b022e1be
--- /dev/null
+++ b/classes/post/post.php
@@ -0,0 +1,718 @@
+.
+
+/**
+ * Class for working with posts
+ *
+ * @package mod_moodleoverflow
+ * @copyright 2023 Tamaro Walter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+namespace mod_moodleoverflow\post;
+
+// Import namespace from the locallib, needs a check later which namespaces are really needed.
+use mod_moodleoverflow\anonymous;
+use mod_moodleoverflow\capabilities;
+use mod_moodleoverflow\review;
+use mod_moodleoverflow\readtracking;
+use mod_moodleoverflow\discussion\discussion;
+use moodle_exception;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/moodleoverflow/locallib.php');
+
+/**
+ * Class that represents a post.
+ *
+ * Please be careful with functions that delete, add or edit posts.
+ * Security checks for these functions were done in the post_control class and these functions should only be accessed that way.
+ * Accessing these functions directly without the checks from the post_control could lead to serious errors.
+ *
+ * Most of the functions in this class are called by moodleoverflow/classes/discussion/discussion.php . The discussion class
+ * manages posts in a moodleoverflow and works like a toplevel class for the post class. If you want to manipulate
+ * (delete, add, edit) posts, please call the functions from the discussion class. To read and obtain information about posts
+ * you are free to choose.
+ *
+ * @package mod_moodleoverflow
+ * @copyright 2023 Tamaro Walter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class post {
+
+ // Attributes. The most important attributes are private and can only be changed by internal functions.
+ // Other attributes can be accessed directly.
+
+ /** @var int The post ID */
+ private $id;
+
+ /** @var int The corresponding discussion ID */
+ private $discussion;
+
+ /** @var int The parent post ID */
+ private $parent;
+
+ /** @var int The ID of the User who wrote the post */
+ private $userid;
+
+ /** @var int Creation timestamp */
+ public $created;
+
+ /** @var int Modification timestamp */
+ public $modified;
+
+ /** @var string The message (content) of the post */
+ public $message;
+
+ /** @var int The message format*/
+ public $messageformat;
+
+ /** @var string Attachment of the post */
+ public $attachment;
+
+ /** @var int Mailed status*/
+ public $mailed;
+
+ /** @var int Review status */
+ public $reviewed;
+
+ /** @var int The time where the post was reviewed*/
+ public $timereviewed;
+
+ // Not database related functions.
+
+ /** @var int This variable is optional, it contains important information for the add_attachment function */
+ public $formattachments;
+
+ /** @var string The subject/title of the Discussion */
+ public $subject;
+
+ /** @var object The discussion where the post is located */
+ public $discussionobject;
+
+ /** @var object The Moodleoverflow where the post is located*/
+ public $moodleoverflowobject;
+
+ /** @var object The course module object */
+ public $cmobject;
+
+ /** @var object The parent post of an answerpost */
+ public $parentpost;
+
+ // Constructors and other builders.
+
+ /**
+ * Constructor to make a new post.
+ * @param int $id The post ID.
+ * @param int $discussion The discussion ID.
+ * @param int $parent The parent post ID.
+ * @param int $userid The user ID that created the post.
+ * @param int $created Creation timestamp
+ * @param int $modified Modification timestamp
+ * @param string $message The message (content) of the post
+ * @param int $messageformat The message format
+ * @param char $attachment Attachment of the post
+ * @param int $mailed Mailed status
+ * @param int $reviewed Review status
+ * @param int $timereviewed The time where the post was reviewed
+ * @param object $formattachments Information about attachments of the post_form
+ */
+ public function __construct($id, $discussion, $parent, $userid, $created, $modified, $message,
+ $messageformat, $attachment, $mailed, $reviewed, $timereviewed, $formattachments = false) {
+ $this->id = $id;
+ $this->discussion = $discussion;
+ $this->parent = $parent;
+ $this->userid = $userid;
+ $this->created = $created;
+ $this->modified = $modified;
+ $this->message = $message;
+ $this->messageformat = $messageformat;
+ $this->attachment = $attachment;
+ $this->mailed = $mailed;
+ $this->reviewed = $reviewed;
+ $this->timereviewed = $timereviewed;
+ $this->formattachments = $formattachments;
+ }
+
+ /**
+ * Builds a Post from a DB record.
+ * Look up database structure for standard values.
+ * @param object $record Data object.
+ * @return object post instance
+ */
+ public static function from_record($record) {
+ $id = null;
+ if (object_property_exists($record, 'id') && $record->id) {
+ $id = $record->id;
+ }
+
+ $discussion = 0;
+ if (object_property_exists($record, 'discussion') && $record->discussion) {
+ $discussion = $record->discussion;
+ }
+
+ $parent = 0;
+ if (object_property_exists($record, 'parent') && $record->parent) {
+ $parent = $record->parent;
+ }
+
+ $userid = 0;
+ if (object_property_exists($record, 'userid') && $record->userid) {
+ $userid = $record->userid;
+ }
+
+ $created = 0;
+ if (object_property_exists($record, 'created') && $record->created) {
+ $created = $record->created;
+ }
+
+ $modified = 0;
+ if (object_property_exists($record, 'modified') && $record->modified) {
+ $modified = $record->modified;
+ }
+
+ $message = '';
+ if (object_property_exists($record, 'message') && $record->message) {
+ $message = $record->message;
+ }
+
+ $messageformat = 0;
+ if (object_property_exists($record, 'messageformat') && $record->messageformat) {
+ $messageformat = $record->messageformat;
+ }
+
+ $attachment = '';
+ if (object_property_exists($record, 'attachment') && $record->attachment) {
+ $attachment = $record->attachment;
+ }
+
+ $mailed = 0;
+ if (object_property_exists($record, 'mailed') && $record->mailed) {
+ $mailed = $record->mailed;
+ }
+
+ $reviewed = 1;
+ if (object_property_exists($record, 'reviewed') && $record->reviewed) {
+ $reviewed = $record->reviewed;
+ }
+
+ $timereviewed = null;
+ if (object_property_exists($record, 'timereviewed') && $record->timereviewed) {
+ $timereviewed = $record->timereviewed;
+ }
+
+ return new self($id, $discussion, $parent, $userid, $created, $modified, $message, $messageformat, $attachment, $mailed,
+ $reviewed, $timereviewed);
+ }
+
+ /**
+ * Function to make a new post without specifying the Post ID.
+ *
+ * @param int $discussion The discussion ID.
+ * @param int $parent The parent post ID.
+ * @param int $userid The user ID that created the post.
+ * @param int $created Creation timestamp
+ * @param int $modified Modification timestamp
+ * @param string $message The message (content) of the post
+ * @param int $messageformat The message format
+ * @param char $attachment Attachment of the post
+ * @param int $mailed Mailed status
+ * @param int $reviewed Review status
+ * @param int $timereviewed The time where the post was reviewed
+ * @param object $formattachments Information about attachments from the post_form
+ *
+ * @return object post object without id
+ */
+ public static function construct_without_id($discussion, $parent, $userid, $created, $modified, $message,
+ $messageformat, $attachment, $mailed, $reviewed, $timereviewed, $formattachments = false) {
+ $id = null;
+ return new self($id, $discussion, $parent, $userid, $created, $modified, $message, $messageformat, $attachment, $mailed,
+ $reviewed, $timereviewed, $formattachments);
+ }
+
+ // Post Functions.
+
+ /**
+ * Adds a new post in an existing discussion.
+ * @return bool|int The Id of the post if operation was successful
+ * @throws coding_exception
+ * @throws dml_exception
+ */
+ public function moodleoverflow_add_new_post() {
+ global $USER, $DB;
+
+ // Add post to the database.
+ $this->id = $DB->insert_record('moodleoverflow_posts', $this->build_db_object());
+ $this->moodleoverflow_add_attachment($this, $this->get_moodleoverflow(), $this->get_coursemodule());
+
+ if ($this->reviewed) {
+ // Update the discussion.
+ $DB->set_field('moodleoverflow_discussions', 'timemodified', $this->modified, ['id' => $this->discussion]);
+ $DB->set_field('moodleoverflow_discussions', 'usermodified', $this->userid, ['id' => $this->discussion]);
+ }
+
+ // Mark the created post as read if the user is tracking the discussion.
+ $cantrack = readtracking::moodleoverflow_can_track_moodleoverflows($this->get_moodleoverflow());
+ $istracked = readtracking::moodleoverflow_is_tracked($this->get_moodleoverflow());
+ if ($cantrack && $istracked) {
+ // Please be aware that in future the use of get_db_object() should be replaced with only $this,
+ // as the readtracking class should be refactored with the new way of working with posts.
+ readtracking::moodleoverflow_mark_post_read($this->userid, $this->get_db_object());
+ }
+
+ // Return the id of the created post.
+ return $this->id;
+ }
+
+ /**
+ * Deletes a single moodleoverflow post.
+ *
+ * @param bool $deletechildren The child posts
+ *
+ * @return bool Whether the deletion was successful or not
+ */
+ public function moodleoverflow_delete_post($deletechildren) {
+ global $DB, $USER;
+ $this->existence_check();
+
+ // Iterate through all children and delete them.
+ // In case something does not work we throw the error as it should be known that something went ... terribly wrong.
+ // All DB transactions are rolled back.
+ try {
+ $transaction = $DB->start_delegated_transaction();
+
+ // Get the coursemoduleid for later use.
+ $coursemoduleid = $this->get_coursemodule()->id;
+ $childposts = $this->moodleoverflow_get_childposts();
+ if ($deletechildren && $childposts) {
+ foreach ($childposts as $childpost) {
+ $child = $this->from_record($childpost);
+ $child->moodleoverflow_delete_post($deletechildren);
+ }
+ }
+
+ // Delete the ratings.
+ $DB->delete_records('moodleoverflow_ratings', ['postid' => $this->id]);
+
+ // Delete the post.
+ if ($DB->delete_records('moodleoverflow_posts', ['id' => $this->id])) {
+ // Delete the read records.
+ readtracking::moodleoverflow_delete_read_records(-1, $this->id);
+
+ // Delete the attachments.
+ $fs = get_file_storage();
+ $context = \context_module::instance($coursemoduleid);
+ $attachments = $fs->get_area_files($context->id, 'mod_moodleoverflow', 'attachment',
+ $this->id, "filename", true);
+ foreach ($attachments as $attachment) {
+ // Get file.
+ $file = $fs->get_file($context->id, 'mod_moodleoverflow', 'attachment', $this->id,
+ $attachment->get_filepath(), $attachment->get_filename());
+ // Delete it if it exists.
+ if ($file) {
+ $file->delete();
+ }
+ }
+
+ // Trigger the post deletion event.
+ $params = [
+ 'context' => $context,
+ 'objectid' => $this->id,
+ 'other' => [
+ 'discussionid' => $this->discussion,
+ 'moodleoverflowid' => $this->get_moodleoverflow()->id,
+ ],
+ ];
+ if ($this->userid !== $USER->id) {
+ $params['relateduserid'] = $this->userid;
+ }
+ $event = \mod_moodleoverflow\event\post_deleted::create($params);
+ $event->trigger();
+
+ // Set the id of this instance to null, so that working with it is not possible anymore.
+ $this->id = null;
+
+ // The post has been deleted.
+ $transaction->allow_commit();
+ return true;
+ }
+ } catch (Exception $e) {
+ $transaction->rollback($e);
+ }
+
+ // Deleting the post failed.
+ return false;
+ }
+
+ /**
+ * Edits the message from this instance.
+ * @param int $time The time the post was modified (given from the discussion class).
+ * @param string $postmessage The new message
+ * @param int $messageformat
+ * @param object $formattachments Information about attachments from the post_form
+ *
+ * @return true if the post has been edited successfully
+ */
+ public function moodleoverflow_edit_post($time, $postmessage, $messageformat, $formattachments) {
+ global $DB;
+ $this->existence_check();
+
+ // Update the attributes.
+ $this->modified = $time;
+ $this->message = $postmessage;
+ $this->messageformat = $messageformat;
+ $this->formattachments = $formattachments;
+
+ // Update the record in the database.
+ $DB->update_record('moodleoverflow_posts', $this->build_db_object());
+
+ // Update the attachments. This happens after the DB update call, as this function changes the DB record as well.
+ $this->moodleoverflow_add_attachment();
+
+ // Mark the edited post as read.
+ $this->mark_post_read();
+
+ // The post has been edited successfully.
+ return true;
+ }
+
+ /**
+ * // NOTE: This function replaces the get_post_full() function but is not used until the print and print-related function for
+ * // printing the discussion and a post are adapted to the new post and discussion class.
+ * Gets a post with all info ready for moodleoverflow_print_post.
+ * Most of these joins are just to get the forum id.
+ *
+ * @return mixed array of posts or false
+ */
+ public function moodleoverflow_get_complete_post() {
+ global $DB, $CFG;
+ $this->existence_check();
+
+ if ($CFG->branch >= 311) {
+ $allnames = \core_user\fields::for_name()->get_sql('u', false, '', '', false)->selects;
+ } else {
+ $allnames = implode(', ', fields::get_name_fields());
+ }
+ $sql = "SELECT p.*, d.moodleoverflow, $allnames, u.email, u.picture, u.imagealt
+ FROM {moodleoverflow_posts} p
+ JOIN {moodleoverflow_discussions} d ON p.discussion = d.id
+ LEFT JOIN {user} u ON p.userid = u.id
+ WHERE p.id = " . $this->id . " ;";
+
+ $post = $DB->get_records_sql($sql);
+ if ($post->userid == 0) {
+ $post->message = get_string('privacy:anonym_post_message', 'mod_moodleoverflow');
+ }
+ return $post;
+ }
+
+ /**
+ * If successful, this function returns the name of the file
+ *
+ * @return bool
+ */
+ public function moodleoverflow_add_attachment() {
+ global $DB;
+ $this->existence_check();
+
+ if (empty($this->formattachments)) {
+ return true; // Nothing to do.
+ }
+
+ $context = \context_module::instance($this->get_coursemodule()->id);
+ $info = file_get_draft_area_info($this->formattachments);
+ $present = ($info['filecount'] > 0) ? '1' : '';
+ file_save_draft_area_files($this->formattachments, $context->id, 'mod_moodleoverflow', 'attachment', $this->id,
+ \mod_moodleoverflow_post_form::attachment_options($this->get_moodleoverflow()));
+ $DB->set_field('moodleoverflow_posts', 'attachment', $present, ['id' => $this->id]);
+ }
+
+ /**
+ * Returns attachments with information for the template
+ *
+ *
+ * @return array
+ */
+ public function moodleoverflow_get_attachments() {
+ global $CFG, $OUTPUT;
+ $this->existence_check();
+
+ if (empty($this->attachment) || (!$context = \context_module::instance($this->get_coursemodule()->id))) {
+ return [];
+ }
+
+ $attachments = [];
+ $fs = get_file_storage();
+
+ // We retrieve all files according to the time that they were created. In the case that several files were uploaded
+ // at the sametime (e.g. in the case of drag/drop upload) we revert to using the filename.
+ $files = $fs->get_area_files($context->id, 'mod_moodleoverflow', 'attachment', $this->id, "filename", false);
+ if ($files) {
+ $i = 0;
+ foreach ($files as $file) {
+ $attachments[$i] = [];
+ $attachments[$i]['filename'] = $file->get_filename();
+ $mimetype = $file->get_mimetype();
+ $iconimage = $OUTPUT->pix_icon(file_file_icon($file),
+ get_mimetype_description($file), 'moodle',
+ ['class' => 'icon']);
+ $path = moodle_url::make_pluginfile_url($file->get_contextid(), $file->get_component(), $file->get_filearea(),
+ $file->get_itemid(), $file->get_filepath(), $file->get_filename());
+ $attachments[$i]['icon'] = $iconimage;
+ $attachments[$i]['filepath'] = $path;
+
+ if (in_array($mimetype, ['image/gif', 'image/jpeg', 'image/png'])) {
+ // Image attachments don't get printed as links.
+ $attachments[$i]['image'] = true;
+ } else {
+ $attachments[$i]['image'] = false;
+ }
+ $i += 1;
+ }
+ }
+ return $attachments;
+ }
+
+ // Getter.
+
+ /**
+ * Getter for the postid
+ * @return int $this->id The post ID.
+ */
+ public function get_id() {
+ $this->existence_check();
+ return $this->id;
+ }
+
+ /**
+ * Getter for the discussionid
+ * @return int $this->discussion The ID of the discussion where the post is located.
+ */
+ public function get_discussionid() {
+ $this->existence_check();
+ return $this->discussion;
+ }
+
+ /**
+ * Getter for the parentid
+ * @return int $this->parent The ID of the parent post.
+ */
+ public function get_parentid() {
+ $this->existence_check();
+ return $this->parent;
+ }
+
+ /**
+ * Getter for the userid
+ * @return int $this->userid The ID of the user who wrote the post.
+ */
+ public function get_userid() {
+ $this->existence_check();
+ return $this->userid;
+ }
+
+ /**
+ * Returns the moodleoverflow where the post is located.
+ * @return object $moodleoverflowobject
+ */
+ public function get_moodleoverflow() {
+ global $DB;
+ $this->existence_check();
+
+ if (empty($this->moodleoverflowobject)) {
+ $discussion = $this->get_discussion();
+ $this->moodleoverflowobject = $DB->get_record('moodleoverflow', ['id' => $discussion->get_moodleoverflowid()]);
+ }
+
+ return $this->moodleoverflowobject;
+ }
+
+ /**
+ * Returns the discussion where the post is located.
+ *
+ * @return object $discussionobject.
+ */
+ public function get_discussion() {
+ global $DB;
+ $this->existence_check();
+
+ if (empty($this->discussionobject)) {
+ $record = $DB->get_record('moodleoverflow_discussions', ['id' => $this->discussion]);
+ $this->discussionobject = discussion::from_record($record);
+ }
+ return $this->discussionobject;
+ }
+
+ /**
+ * Returns the coursemodule
+ *
+ * @return object $cmobject
+ */
+ public function get_coursemodule() {
+ $this->existence_check();
+
+ if (empty($this->cmobject)) {
+ $this->cmobject = \get_coursemodule_from_instance('moodleoverflow', $this->get_moodleoverflow()->id);
+ }
+
+ return $this->cmobject;
+ }
+
+ /**
+ * Returns the parent post
+ * @return object|false $post|false
+ */
+ public function moodleoverflow_get_parentpost() {
+ global $DB;
+ $this->existence_check();
+
+ if ($this->parent == 0) {
+ // This post is the parent post.
+ $this->parentpost = false;
+ return false;
+ }
+
+ if (empty($this->parentpost)) {
+ $parentpostrecord = $DB->get_record('moodleoverflow_post', ['id' => $this->parent]);
+ $this->parentpost = $this->from_record($parentpostrecord);
+ }
+ return $this->parentpost;
+ }
+
+ /**
+ * Returns children posts (answers) as DB-records.
+ *
+ * @return array|false children/answer posts.
+ */
+ public function moodleoverflow_get_childposts() {
+ global $DB;
+ $this->existence_check();
+
+ if ($childposts = $DB->get_records('moodleoverflow_posts', ['parent' => $this->id])) {
+ return $childposts;
+ }
+
+ return false;
+ }
+
+ /**
+ * This getter works as an help function in case another file/function needs the db-object of this instance (as the function
+ * is not adapted/refactored to the new way of working with discussion).
+ * @return object
+ */
+ public function get_db_object() {
+ $this->existence_check();
+ return $this->build_db_object();
+ }
+
+ // Helper Functions.
+
+ /**
+ * Calculate the ratings of a post.
+ *
+ * @return object $ratingsobject.
+ */
+ public function moodleoverflow_get_post_ratings() {
+ $this->existence_check();
+
+ $discussionid = $this->get_discussion()->id;
+ $postratings = \mod_moodleoverflow\ratings::moodleoverflow_get_ratings_by_discussion($discussionid, $this->id);
+
+ $ratingsobject = new \stdClass();
+ $ratingsobject->upvotes = $postratings->upvotes;
+ $ratingsobject->downvotes = $postratings->downvotes;
+ $ratingsobject->votesdifference = $postratings->upvotes - $postratings->downvotes;
+ $ratingsobject->markedhelpful = $postratings->ishelpful;
+ $ratingsobject->markedsolution = $postratings->issolved;
+
+ return $ratingsobject;
+ }
+
+ /**
+ * Marks the post as read if the user is tracking the discussion.
+ * Uses function from mod_moodleoverflow\readtracking.
+ */
+ public function mark_post_read() {
+ global $USER;
+ $cantrack = readtracking::moodleoverflow_can_track_moodleoverflows($this->get_moodleoverflow());
+ $istracked = readtracking::moodleoverflow_is_tracked($this->get_moodleoverflow());
+ if ($cantrack && $istracked) {
+ // Please be aware that in future the use of get_db_object() should be replaced with only $this,
+ // as the readtracking class should be refactored with the new way of working with posts.
+ readtracking::moodleoverflow_mark_post_read($USER->id, $this->get_db_object());
+ }
+ }
+
+ /**
+ * Builds an object from this instance that has only DB-relevant attributes.
+ * @return object $dbobject
+ */
+ private function build_db_object() {
+ $dbobject = new \stdClass();
+ $dbobject->id = $this->id;
+ $dbobject->discussion = $this->discussion;
+ $dbobject->parent = $this->parent;
+ $dbobject->userid = $this->userid;
+ $dbobject->created = $this->created;
+ $dbobject->modified = $this->modified;
+ $dbobject->message = $this->message;
+ $dbobject->messageformat = $this->messageformat;
+ $dbobject->attachment = $this->attachment;
+ $dbobject->mailed = $this->mailed;
+ $dbobject->reviewed = $this->reviewed;
+ $dbobject->timereviewed = $this->timereviewed;
+
+ return $dbobject;
+ }
+
+ /**
+ * Count all replies of a post.
+ *
+ * @param bool $onlyreviewed Whether to count only reviewed posts.
+ * @return int Amount of replies
+ */
+ public function moodleoverflow_count_replies($onlyreviewed) {
+ global $DB;
+
+ $conditions = ['parent' => $this->id];
+
+ if ($onlyreviewed) {
+ $conditions['reviewed'] = '1';
+ }
+
+ // Return the amount of replies.
+ return $DB->count_records('moodleoverflow_posts', $conditions);
+ }
+
+ // Security.
+
+ /**
+ * Makes sure that the instance exists in the database. Every function in this class requires this check
+ * (except the function that adds a post to the database)
+ *
+ * @return true
+ * @throws moodle_exception
+ */
+ private function existence_check() {
+ if (empty($this->id) || $this->id == false || $this->id == null) {
+ throw new moodle_exception('noexistingpost', 'moodleoverflow');
+ }
+ return true;
+ }
+}
diff --git a/classes/post/post_control.php b/classes/post/post_control.php
new file mode 100644
index 0000000000..2e692c3060
--- /dev/null
+++ b/classes/post/post_control.php
@@ -0,0 +1,889 @@
+.
+
+/**
+ * Class that is important to interact with posts.
+ *
+ * @package mod_moodleoverflow
+ * @copyright 2023 Tamaro Walter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+namespace mod_moodleoverflow\post;
+
+// Import namespace from the locallib, needs a check later which namespaces are really needed.
+use mod_moodleoverflow\anonymous;
+use mod_moodleoverflow\capabilities;
+use mod_moodleoverflow\event\discussion_created;
+use mod_moodleoverflow\event\post_created;
+use mod_moodleoverflow\event\post_updated;
+use mod_moodleoverflow\review;
+
+use mod_moodleoverflow\post\post;
+use mod_moodleoverflow\discussion\discussion;
+use mod_moodleoverflow\subscriptions;
+use moodle_exception;
+use stdClass;
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+
+require_once($CFG->dirroot . '/mod/moodleoverflow/locallib.php');
+require_once($CFG->libdir . '/completionlib.php');
+
+/**
+ * This Class controls the manipulation of posts and acts as controller of interactions with the post.php
+ *
+ * This Class has 2 main Tasks:
+ * 1. Before entering the post.php
+ * - Detect the wanted interaction (new discussion, new answer in a discussion, editing or deleting a post)
+ * - make capability and other security/integrity checks (are all given data correct?)
+ * - gather important information that need to be used later.
+ * Note: if a post is being deleted, the post_control deletes it in the first step and the post.php does not call the post_form.php
+ *
+ * Now the post.php calls the post_form, so that the user can enter a message and attachments.
+ *
+ * 2. After calling the post_form:
+ * - collect the information from the post_form
+ * - based on the interaction, call the right function
+ *
+ * @package mod_moodleoverflow
+ * @copyright 2023 Tamaro Walter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class post_control {
+
+ /** @var string the Interaction type, the interactions are:
+ * - create (creates a new discussion with a first post)
+ * - reply (replies to a existing post, can be an answer or a comment)
+ * - edit (change the content of an existing post)
+ * - delete (delete a post from a discussion)
+ */
+ private $interaction;
+
+ /** @var object information about the post like the related moodleoverflow, post etc.
+ * Difference between info and prepost: Info has objects, prepost mostly ID's and string like the message of the post.
+ */
+ private $info;
+
+ /** @var object prepost for the classes/post/post_form.php,
+ * this object is more like a prototype of a post and it's not in the database*
+ * Difference between info and prepost. Info has objects, prepost mostly ID's and strings like the message of the post.
+ */
+ private $prepost;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->info = new stdClass();
+ }
+
+ /**
+ * Detects the interaction and builds the prepost.
+ * @param stdClass $urlparameter parameter from the post.php
+ * @throws \coding_exception
+ * @throws moodle_exception if the interaction is not correct.
+ */
+ public function detect_interaction($urlparameter) {
+ $count = 0;
+ $count += $urlparameter->create ? 1 : 0;
+ $count += $urlparameter->reply ? 1 : 0;
+ $count += $urlparameter->edit ? 1 : 0;
+ $count += $urlparameter->delete ? 1 : 0;
+ if ($count !== 1) {
+ throw new \coding_exception('Exactly one parameter should be specified!');
+ }
+
+ if ($urlparameter->create) {
+ $this->interaction = 'create';
+ $this->info->moodleoverflowid = $urlparameter->create;
+ $this->build_prepost_create($this->info->moodleoverflowid);
+
+ } else if ($urlparameter->edit) {
+ $this->interaction = 'edit';
+ $this->info->editpostid = $urlparameter->edit;
+ $this->build_prepost_edit($this->info->editpostid);
+
+ } else if ($urlparameter->reply) {
+ $this->interaction = 'reply';
+ $this->info->replypostid = $urlparameter->reply;
+ $this->build_prepost_reply($this->info->replypostid);
+
+ } else if ($urlparameter->delete) {
+ $this->interaction = 'delete';
+ $this->info->deletepostid = $urlparameter->delete;
+ $this->build_prepost_delete($this->info->deletepostid);
+ } else {
+ throw new moodle_exception('unknownaction');
+ }
+ }
+
+ /**
+ * Controls the execution of an interaction.
+ * @param object $form The results from the post_form.
+ */
+ public function execute_interaction($form) {
+ global $CFG;
+ // Redirect url in case of occurring errors.
+ if (empty($SESSION->fromurl)) {
+ $errordestination = $CFG->wwwroot . '/mod/moodleoverflow/view.php?m=' . $this->prepost->moodleoverflowid;
+ } else {
+ $errordestination = $SESSION->fromurl;
+ }
+
+ // Format the submitted data.
+ $this->prepost->messageformat = $form->message['format'];
+ $this->prepost->formattachments = $form->attachments;
+ $this->prepost->message = $form->message['text'];
+ $this->prepost->messagetrust = trusttext_trusted($this->prepost->modulecontext);
+
+ // Get the current time.
+ $this->prepost->timenow = time();
+
+ // Execute the right function.
+ if ($this->interaction == 'create' && $form->moodleoverflow == $this->prepost->moodleoverflowid) {
+ $this->execute_create($form, $errordestination);
+ } else if ($this->interaction == 'reply' && $form->reply == $this->prepost->parentid) {
+ $this->execute_reply($form, $errordestination);
+ } else if ($this->interaction == 'edit' && $form->edit == $this->prepost->postid) {
+ $this->execute_edit($form, $errordestination);
+ } else {
+ throw new moodle_exception('unexpectedinteractionerror', 'moodleoverflow', $errordestination);
+ }
+ }
+
+ /**
+ * This function is used when a guest enters the post.php.
+ * Parameters will be checked so that the post.php can redirect the user to the right site.
+ * @param int $postid
+ * @param int $moodleoverflowid
+ * @return object $this->information // The gathered information.
+ */
+ public function catch_guest($postid = false, $moodleoverflowid = false) {
+ global $PAGE;
+ if ((!$postid && !$moodleoverflowid) || ($postid && $moodleoverflowid)) {
+ throw new moodle_exception('inaccurateparameter', 'moodleoverflow');
+ }
+ if ($postid) {
+ $this->collect_information($postid, false);
+ } else if ($moodleoverflowid) {
+ $this->collect_information(false, $moodleoverflowid);
+ }
+ $this->info->modulecontext = \context_module::instance($this->info->cm->id);
+
+ // Set the parameters for the page.
+ $PAGE->set_cm($this->info->cm, $this->info->course, $this->info->moodleoverflow);
+ $PAGE->set_context($this->info->modulecontext);
+ $PAGE->set_title($this->info->course->shortname);
+ $PAGE->set_heading($this->info->course->fullname);
+ $PAGE->add_body_class('limitedwidth');
+ return $this->info;
+ }
+
+ // Build functions, that build the prepost object for further use.
+
+ /**
+ * Function to prepare a new discussion in moodleoverflow.
+ *
+ * @param int $moodleoverflowid The ID of the moodleoverflow where the new discussion post is being created.
+ */
+ private function build_prepost_create($moodleoverflowid) {
+ global $DB, $SESSION, $USER;
+
+ // Get the related moodleoverflow, course coursemodule and the contexts.
+ $this->collect_information(false, $moodleoverflowid);
+
+ // Check if the user can start a new discussion.
+ if (!$this->check_user_can_create_discussion()) {
+
+ // Catch unenrolled user.
+ if (!isguestuser() && !is_enrolled($this->info->coursecontext)) {
+ if (enrol_selfenrol_available($this->info->course->id)) {
+ $SESSION->wantsurl = qualified_me();
+ $SESSION->enrolcancel = get_local_referer(false);
+ redirect(new \moodle_url('/enrol/index.php', ['id' => $this->info->course->id,
+ 'returnurl' => '/mod/moodleoverflow/view.php?m=' . $this->info->moodleoverflow->id, ]),
+ get_string('youneedtoenrol'));
+ }
+ }
+ // Notify the user, that he can not post a new discussion.
+ throw new moodle_exception('nopostmoodleoverflow', 'moodleoverflow');
+ }
+
+ // Where is the user coming from?
+ $SESSION->fromurl = get_local_referer(false);
+
+ // Prepare the post.
+ $this->assemble_prepost();
+ $this->prepost->postid = null;
+ $this->prepost->discussionid = null;
+ $this->prepost->parentid = 0;
+ $this->prepost->subject = '';
+ $this->prepost->userid = $USER->id;
+ $this->prepost->message = '';
+
+ // Unset where the user is coming from.
+ // Allows to calculate the correct return url later.
+ unset($SESSION->fromdiscussion);
+ }
+
+ /**
+ * Function to prepare a new post that replies to an existing post.
+ *
+ * @param int $replypostid The ID of the post that is being answered.
+ */
+ private function build_prepost_reply($replypostid) {
+ global $DB, $PAGE, $SESSION, $USER, $CFG;
+
+ // Get the related poost, discussion, moodleoverflow, course, coursemodule and contexts.
+ $this->collect_information($replypostid, false);
+
+ // Ensure the coursemodule is set correctly.
+ $PAGE->set_cm($this->info->cm, $this->info->course, $this->info->moodleoverflow);
+
+ // Prepare a post.
+ $this->assemble_prepost();
+ $this->prepost->postid = null;
+ $this->prepost->parentid = $this->info->relatedpost->get_id();
+ $this->prepost->userid = $USER->id;
+ $this->prepost->message = '';
+
+ // Check whether the user is allowed to post.
+ if (!$this->check_user_can_create_reply()) {
+
+ // Give the user the chance to enroll himself to the course.
+ if (!isguestuser() && !is_enrolled($this->info->coursecontext)) {
+ $SESSION->wantsurl = qualified_me();
+ $SESSION->enrolcancel = get_local_referer(false);
+ redirect(new \moodle_url('/enrol/index.php',
+ ['id' => $this->info->course->id,
+ 'returnurl' => '/mod/moodleoverflow/view.php?m=' . $this->info->moodleoverflow->id,
+ ]), get_string('youneedtoenrol'));
+ }
+ // Print the error message.
+ throw new moodle_exception('nopostmoodleoverflow', 'moodleoverflow');
+ }
+ // Make sure the user can post here.
+ if (!$this->info->cm->visible && !has_capability('moodle/course:viewhiddenactivities', $this->info->modulecontext)) {
+ throw new moodle_exception('activityiscurrentlyhidden');
+ }
+
+ // Append 'RE: ' to the discussions subject.
+ $strre = get_string('re', 'moodleoverflow');
+ if (check_php_version('8.0.0')) {
+ if (!(str_starts_with($this->prepost->subject, $strre))) {
+ $this->prepost->subject = $strre . ' ' . $this->prepost->subject;
+ }
+ } else {
+ // LEARNWEB-TODO: remove this else branch when support for php version 7.4 ends.
+ if (!(substr($this->prepost->subject, 0, strlen($strre)) == $strre)) {
+ $this->prepost->subject = $strre . ' ' . $this->prepost->subject;
+ }
+ }
+
+ // Unset where the user is coming from.
+ // Allows to calculate the correct return url later.
+ unset($SESSION->fromdiscussion);
+ }
+
+ /**
+ * Function to prepare the edit of an user own existing post.
+ *
+ * @param int $editpostid The ID of the post that is being edited.
+ */
+ private function build_prepost_edit($editpostid) {
+ global $DB, $PAGE, $SESSION, $USER;
+
+ // Get the related post, discussion, moodleoverflow, course, coursemodule and contexts.
+ $this->collect_information($editpostid, false);
+
+ // Set the pages context.
+ $PAGE->set_cm($this->info->cm, $this->info->course, $this->info->moodleoverflow);
+
+ // Check if the post can be edited.
+ $beyondtime = ((time() - $this->info->relatedpost->created) > get_config('moodleoverflow', 'maxeditingtime'));
+
+ // Please be aware that in future the use of get_db_object() should be replaced with $this->info->relatedpost,
+ // as the review class should be refactored with the new way of working with posts.
+ $alreadyreviewed = review::should_post_be_reviewed($this->info->relatedpost->get_db_object(), $this->info->moodleoverflow)
+ && $this->info->relatedpost->reviewed;
+ if (($beyondtime || $alreadyreviewed) && !has_capability('mod/moodleoverflow:editanypost',
+ $this->info->modulecontext)) {
+ throw new moodle_exception('maxtimehaspassed', 'moodleoverflow', '',
+ format_time(get_config('moodleoverflow', 'maxeditingtime')));
+ }
+
+ // If the current user is not the one who posted this post.
+ if ($this->info->relatedpost->get_userid() != $USER->id) {
+
+ // Check if the current user has not the capability to edit any post.
+ if (!has_capability('mod/moodleoverflow:editanypost', $this->info->modulecontext)) {
+
+ // Display the error. Capabilities are missing.
+ throw new moodle_exception('cannoteditposts', 'moodleoverflow');
+ }
+ }
+
+ // Load the $post variable.
+ $this->assemble_prepost();
+
+ // Unset where the user is coming from. This allows to calculate the correct return url later.
+ unset($SESSION->fromdiscussion);
+ }
+
+ /**
+ * Function to prepare the deletion of a post.
+ *
+ * @param int $deletepostid The ID of the post that is being deleted.
+ */
+ private function build_prepost_delete($deletepostid) {
+ global $DB, $USER;
+
+ // Get the related post, discussion, moodleoverflow, course, coursemodule and contexts.
+ $this->collect_information($deletepostid, false);
+
+ // Require a login and retrieve the modulecontext.
+ require_login($this->info->course, false, $this->info->cm);
+
+ // Check some capabilities.
+ $this->check_user_can_delete_post();
+
+ // Count all replies of this post.
+ $this->info->replycount = $this->info->relatedpost->moodleoverflow_count_replies(false);
+ if ($this->info->replycount >= 1) {
+ $this->info->deletetype = 'plural';
+ } else {
+ $this->info->deletetype = 'singular';
+ }
+ // Build the prepost.
+ $this->assemble_prepost();
+ $this->prepost->deletechildren = true;
+ }
+
+ // Execute Functions.
+
+ /**
+ * Executes the creation of a new discussion.
+ *
+ * @param object $form The results from the post_form.
+ * @param string $errordestination The URL to redirect to in case of an error.
+ * @throws moodle_exception if the discussion could not be added.
+ */
+ private function execute_create($form, $errordestination) {
+ global $USER;
+ // Check if the user is allowed to post.
+ $this->check_user_can_create_discussion();
+
+ // Set the post to not reviewed if questions should be reviewed and the user is not a reviewed themselves.
+ if (review::get_review_level($this->info->moodleoverflow) >= review::QUESTIONS &&
+ !capabilities::has(capabilities::REVIEW_POST, $this->info->modulecontext, $USER->id)) {
+ $this->prepost->reviewed = 0;
+ } else {
+ $this->prepost->reviewed = 1;
+ }
+
+ // Get the discussion subject.
+ $this->prepost->subject = $form->subject;
+
+ // Create the discussion object.
+ $discussion = discussion::construct_without_id($this->prepost->courseid, $this->prepost->moodleoverflowid,
+ $this->prepost->subject, 0, $this->prepost->userid,
+ $this->prepost->timenow, $this->prepost->timenow, $this->prepost->userid);
+ if (!$discussion->moodleoverflow_add_discussion($this->prepost)) {
+ throw new moodle_exception('couldnotadd', 'moodleoverflow', $errordestination);
+ }
+
+ // The creation was successful.
+ $redirectmessage = \html_writer::tag('p', get_string("postaddedsuccess", "moodleoverflow"));
+
+ // Trigger the discussion created event.
+ $params = ['context' => $this->info->modulecontext, 'objectid' => $discussion->get_id()];
+ $event = discussion_created::create($params);
+ $event->trigger();
+
+ // Subscribe to this thread.
+ // Please be aware that in future the use of get_db_object() should be replaced with only $this->info->discussion,
+ // as the subscription class should be refactored with the new way of working with posts.
+ subscriptions::moodleoverflow_post_subscription($form, $this->info->moodleoverflow,
+ $discussion->get_db_object(),
+ $this->info->modulecontext);
+
+ // Define the location to redirect the user after successfully posting.
+ $redirectto = new \moodle_url('/mod/moodleoverflow/view.php', ['m' => $form->moodleoverflow]);
+ redirect(moodleoverflow_go_back_to($redirectto->out()), $redirectmessage, null, \core\output\notification::NOTIFY_SUCCESS);
+ }
+
+ /**
+ * Executes the reply to an existing post.
+ *
+ * @param object $form The results from the post_form.
+ * @param string $errordestination The URL to redirect to in case of an error.
+ * @throws moodle_exception if the reply could not be added.
+ */
+ private function execute_reply($form, $errordestination) {
+ // Check if the user has the capability to write a reply.
+ $this->check_user_can_create_reply();
+
+ // Set to not reviewed, if posts should be reviewed, and user is not a reviewer themselves.
+ if (review::get_review_level($this->info->moodleoverflow) == review::EVERYTHING &&
+ !has_capability('mod/moodleoverflow:reviewpost', \context_module::instance($this->info->cm->id))) {
+ $this->prepost->reviewed = 0;
+ } else {
+ $this->prepost->reviewed = 1;
+ }
+
+ // Create the new post.
+ if (!$newpostid = $this->info->discussion->moodleoverflow_add_post_to_discussion($this->prepost)) {
+ throw new moodle_exception('couldnotadd', 'moodleoverflow', $errordestination);
+ }
+
+ // The creation was successful.
+ $redirectmessage = \html_writer::tag('p', get_string("postaddedsuccess", "moodleoverflow"));
+ $redirectmessage .= \html_writer::tag('p', get_string("postaddedtimeleft", "moodleoverflow",
+ format_time(get_config('moodleoverflow', 'maxeditingtime'))));
+
+ // Trigger the post created event.
+ $params = ['context' => $this->info->modulecontext, 'objectid' => $newpostid,
+ 'other' => ['discussionid' => $this->prepost->discussionid,
+ 'moodleoverflowid' => $this->prepost->moodleoverflowid,
+ ],
+ ];
+ $event = post_created::create($params);
+ $event->trigger();
+
+ // Subscribe to this thread.
+ // Please be aware that in future the use of build_db_object() should be replaced with only $this->info->discussion,
+ // as the subscription class should be refactored with the new way of working with posts.
+ subscriptions::moodleoverflow_post_subscription($form, $this->info->moodleoverflow,
+ $this->info->discussion->get_db_object(),
+ $this->info->modulecontext);
+
+ // Define the location to redirect the user after successfully posting.
+ $redirectto = new \moodle_url('/mod/moodleoverflow/discussion.php',
+ ['d' => $this->prepost->discussionid, 'p' => $newpostid]);
+ redirect(\moodleoverflow_go_back_to($redirectto->out()), $redirectmessage, null, \core\output\notification::NOTIFY_SUCCESS);
+
+ }
+
+ /**
+ * Executes the edit of an existing post.
+ *
+ * @param object $form The results from the post_form.
+ * @param string $errordestination The URL to redirect to in case of an error.
+ * @throws moodle_exception if the post could not be updated.
+ */
+ private function execute_edit($form, $errordestination) {
+ global $USER, $DB;
+ // Check if the user has the capability to edit his post.
+ $this->check_user_can_edit_post();
+
+ // If the post that is being edited is the parent post, the subject can be edited too.
+ if ($this->prepost->parentid == 0) {
+ $this->prepost->subject = $form->subject;
+ }
+
+ // Update the post.
+ if (!$this->info->discussion->moodleoverflow_edit_post_from_discussion($this->prepost)) {
+ throw new moodle_exception('couldnotupdate', 'moodleoverflow', $errordestination);
+ }
+
+ // The edit was successful.
+ $redirectmessage = get_string('postupdated', 'moodleoverflow');
+ if ($this->prepost->userid == $USER->id) {
+ $redirectmessage = get_string('postupdated', 'moodleoverflow');
+ } else {
+ if (anonymous::is_post_anonymous($this->info->discussion, $this->info->moodleoverflow, $this->prepost->userid)) {
+ $name = get_string('anonymous', 'moodleoverflow');
+ } else {
+ $realuser = $DB->get_record('user', ['id' => $this->prepost->userid]);
+ $name = fullname($realuser);
+ }
+ $redirectmessage = get_string('editedpostupdated', 'moodleoverflow', $name);
+ }
+
+ // Trigger the post updated event.
+ $params = ['context' => $this->info->modulecontext, 'objectid' => $form->edit,
+ 'other' => ['discussionid' => $this->prepost->discussionid,
+ 'moodleoverflowid' => $this->prepost->moodleoverflowid,
+ ],
+ 'relateduserid' => $this->prepost->userid == $USER->id ? $this->prepost->userid : null,
+ ];
+ $event = post_updated::create($params);
+ $event->trigger();
+
+ // Define the location to redirect the user after successfully editing.
+ $redirectto = new \moodle_url('/mod/moodleoverflow/discussion.php',
+ ['d' => $this->prepost->discussionid, 'p' => $form->edit]);
+ redirect(moodleoverflow_go_back_to($redirectto->out()), $redirectmessage, null, \core\output\notification::NOTIFY_SUCCESS);
+ }
+
+ /**
+ * Executes the deletion of a post.
+ *
+ * @throws moodle_exception if the post could not be deleted.
+ */
+ public function execute_delete() {
+ $this->check_interaction('delete');
+
+ // Check if the user has the capability to delete the post.
+ $timepassed = time() - $this->info->relatedpost->created;
+ $url = new \moodle_url('/mod/moodleoverflow/discussion.php', ['d' => $this->info->discussion->get_id()]);
+ if (($timepassed > get_config('moodleoverflow', 'maxeditingtime')) && !$this->info->deleteanypost) {
+ throw new moodle_exception('cannotdeletepost', 'moodleoverflow', moodleoverflow_go_back_to($url));
+ }
+
+ // A normal user cannot delete his post if there are direct replies.
+ if ($this->info->replycount && !$this->info->deleteanypost) {
+ throw new moodle_exception('cannotdeletereplies', 'moodleoverflow', moodleoverflow_go_back_to($url));
+ }
+
+ // Check if the post is a parent post or not.
+ if ($this->prepost->parentid == 0) {
+ // Save the moodleoverflowid. Then delete the discussion.
+ $moodleoverflowid = $this->info->discussion->get_moodleoverflowid();
+ $this->info->discussion->moodleoverflow_delete_discussion($this->prepost);
+
+ // Redirect the user back to the start page of the moodleoverflow instance.
+ redirect('view.php?m=' . $moodleoverflowid);
+ } else {
+ $this->info->discussion->moodleoverflow_delete_post_from_discussion($this->prepost);
+ $discussionurl = new \moodle_url('/mod/moodleoverflow/discussion.php', ['d' => $this->info->discussion->get_id()]);
+ redirect(moodleoverflow_go_back_to($discussionurl));
+ }
+ }
+
+ // Functions that uses the post.php to build the page.
+
+ /**
+ * Builds a part of confirmation page. The confirmation request box is being build by the post.php.
+ */
+ public function confirm_delete() {
+ $this->check_interaction('delete');
+ global $PAGE;
+ moodleoverflow_set_return();
+ $PAGE->navbar->add(get_string('delete', 'moodleoverflow'));
+ $PAGE->set_title($this->info->course->shortname);
+ $PAGE->set_heading($this->info->course->fullname);
+ $PAGE->add_body_class('limitedwidth');
+ }
+
+ /**
+ *
+ * Builds and returns a post_form object where the users enters/edits the message and attachments of the post.
+ * @param array $pageparams An object that the post.php created.
+ * @return object a mod_moodleoverflow_post_form object.
+ */
+ public function build_postform($pageparams) {
+ global $USER, $CFG;
+ // Require that the user is logged in properly and enrolled to the course.
+ require_login($this->info->course, false, $this->info->cm);
+
+ // Prepare the attachments.
+ $draftitemid = file_get_submitted_draft_itemid('attachments');
+ file_prepare_draft_area($draftitemid, $this->info->modulecontext->id, 'mod_moodleoverflow', 'attachment',
+ empty($this->prepost->postid) ? null : $this->prepost->postid,
+ \mod_moodleoverflow_post_form::attachment_options($this->info->moodleoverflow));
+
+ // If the post is anonymous, attachments should have an anonymous author when editing the attachment.
+ if ($draftitemid && $this->interaction == 'edit' && anonymous::is_post_anonymous($this->info->discussion,
+ $this->info->moodleoverflow, $this->prepost->userid)) {
+ $usercontext = \context_user::instance($USER->id);
+ $anonymousstr = get_string('anonymous', 'moodleoverflow');
+ foreach (get_file_storage()->get_area_files($usercontext->id, 'user', 'draft', $draftitemid) as $file) {
+ $file->set_author($anonymousstr);
+ }
+ }
+
+ // Prepare the form.
+ $edit = $this->interaction == 'edit';
+ $formarray = ['course' => $this->info->course, 'cm' => $this->info->cm, 'coursecontext' => $this->info->coursecontext,
+ 'modulecontext' => $this->info->modulecontext, 'moodleoverflow' => $this->info->moodleoverflow,
+ 'post' => $this->prepost, 'edit' => $edit,
+ ];
+
+ // Declare the post_form.
+ $mformpost = new \mod_moodleoverflow_post_form('post.php', $formarray, 'post', '', ['id' => 'mformmoodleoverflow']);
+
+ // If the user is not the original author append an extra message to the message. (Happens when interaction = 'edit').
+ if ($USER->id != $this->prepost->userid) {
+ // Create a temporary object.
+ $data = new stdClass();
+ $data->date = userdate(time());
+ $this->prepost->messageformat = editors_get_preferred_format();
+ if ($this->prepost->messageformat == FORMAT_HTML) {
+ $data->name = \html_writer::tag('a', $CFG->wwwroot . '/user/view.php?id' . $USER->id .
+ '&course=' . $this->prepost->courseid . '">' . fullname($USER));
+ $this->prepost->message .= \html_writer::tag('p', \html_writer::tag('span',
+ get_string('editedby', 'moodleoverflow', $data), ["class" => "edited"]));
+ } else {
+ $data->name = fullname($USER);
+ $this->prepost->message .= "\n\n(" . get_string('editedby', 'moodleoverflow', $data) . ')';
+ }
+ // Delete the temporary object.
+ unset($data);
+ }
+
+ // Define the heading for the form.
+ $formheading = '';
+ if ($this->interaction == 'reply') {
+ $heading = get_string('yourreply', 'moodleoverflow');
+ $formheading = get_string('reply', 'moodleoverflow');
+ } else {
+ $heading = get_string('yournewtopic', 'moodleoverflow');
+ }
+
+ // Set data for the form.
+ $mformpost->set_data([
+ 'attachments' => $draftitemid,
+ 'general' => $heading,
+ 'subject' => $this->prepost->subject,
+ 'message' => ['text' => $this->prepost->message,
+ 'format' => editors_get_preferred_format(),
+ 'itemid' => $this->prepost->postid, ],
+ 'userid' => $this->prepost->userid,
+ 'parent' => $this->prepost->parentid,
+ 'discussion' => $this->prepost->discussionid,
+ 'course' => $this->prepost->courseid,
+ ]
+ + $pageparams
+ );
+
+ return $mformpost;
+ }
+
+ // Helper functions.
+
+ // Getter.
+
+ /**
+ * Returns the interaction type.
+ * @return string $interaction
+ */
+ public function get_interaction() {
+ return $this->interaction;
+ }
+
+ /**
+ * Returns the gathered important information in the build_prepost_() functions.
+ * @return object $info
+ */
+ public function get_information() {
+ return $this->info;
+ }
+
+ /**
+ * Retuns the prepared post.
+ * @return object $prepost
+ */
+ public function get_prepost() {
+ return $this->prepost;
+ }
+
+ // Functions that build the info and prepost object.
+
+ /**
+ * Builds the information object that is being used in the build prepost functions.
+ * The variables are optional, but one is necessary to build the information object.
+ * @param int $postid
+ * @param int $moodleoverflowid
+ */
+ private function collect_information($postid = false, $moodleoverflowid = false) {
+ if ($postid) {
+ // The related post is the post that is being answered, edited, or deleted.
+ $this->info->relatedpost = $this->check_post_exists($postid);
+ $this->info->discussion = $this->check_discussion_exists($this->info->relatedpost->get_discussionid());
+ $localmoodleoverflowid = $this->info->discussion->get_moodleoverflowid();
+ } else {
+ $localmoodleoverflowid = $moodleoverflowid;
+ }
+ $this->info->moodleoverflow = $this->check_moodleoverflow_exists($localmoodleoverflowid);
+ $this->info->course = $this->check_course_exists($this->info->moodleoverflow->course);
+ $this->info->cm = $this->check_coursemodule_exists($this->info->moodleoverflow->id, $this->info->course->id);
+ $this->info->modulecontext = \context_module::instance($this->info->cm->id);
+ $this->info->coursecontext = \context_course::instance($this->info->course->id);
+ }
+
+ /**
+ * Assembles the prepost object. Helps to reduce code in the build_prepost functions.
+ * Some prepost parameters will be assigned individually by the build_prepost functions.
+ */
+ private function assemble_prepost() {
+ $this->prepost = new stdClass();
+ $this->prepost->courseid = $this->info->course->id;
+ $this->prepost->moodleoverflowid = $this->info->moodleoverflow->id;
+ $this->prepost->modulecontext = $this->info->modulecontext;
+
+ if ($this->interaction != 'create') {
+ $this->prepost->discussionid = $this->info->discussion->get_id();
+ $this->prepost->subject = $this->info->discussion->name;
+
+ if ($this->interaction != 'reply') {
+ $this->prepost->parentid = $this->info->relatedpost->get_parentid();
+ $this->prepost->postid = $this->info->relatedpost->get_id();
+ $this->prepost->userid = $this->info->relatedpost->get_userid();
+ $this->prepost->message = $this->info->relatedpost->message;
+ }
+ }
+ }
+
+
+ // Interaction check.
+
+ /**
+ * Checks if the interaction is correct
+ * @param string $interaction
+ * @return true if the interaction is correct
+ */
+ private function check_interaction($interaction) {
+ if ($this->interaction != $interaction) {
+ throw new moodle_exception('wronginteraction' , 'moodleoverflow');
+ }
+ return true;
+ }
+
+ // Database checks.
+
+ /**
+ * Checks if the course exists. Returns the $DB->record of the course.
+ * @param int $courseid
+ * @return object $course
+ */
+ private function check_course_exists($courseid) {
+ global $DB;
+ if (!$course = $DB->get_record('course', ['id' => $courseid])) {
+ throw new moodle_exception('invalidcourseid');
+ }
+ return $course;
+ }
+
+ /**
+ * Checks if the coursemodule exists.
+ * @param int $moodleoverflowid
+ * @param int $courseid
+ * @return object $cm
+ */
+ private function check_coursemodule_exists($moodleoverflowid, $courseid) {
+ if (!$cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflowid,
+ $courseid)) {
+ throw new moodle_exception('invalidcoursemodule');
+ }
+ return $cm;
+ }
+
+ /**
+ * Checks if the related moodleoverflow exists.
+ * @param int $moodleoverflowid
+ * @return object $moodleoverflow
+ */
+ private function check_moodleoverflow_exists($moodleoverflowid) {
+ // Get the related moodleoverflow instance.
+ global $DB;
+ if (!$moodleoverflow = $DB->get_record('moodleoverflow', ['id' => $moodleoverflowid])) {
+ throw new moodle_exception('invalidmoodleoverflowid', 'moodleoverflow');
+ }
+ return $moodleoverflow;
+ }
+
+ /**
+ * Checks if the related discussion exists.
+ * @param int $discussionid
+ * @return object $discussion
+ */
+ private function check_discussion_exists($discussionid) {
+ global $DB;
+ if (!$discussionrecord = $DB->get_record('moodleoverflow_discussions', ['id' => $discussionid])) {
+ throw new moodle_exception('invaliddiscussionid', 'moodleoverflow');
+ }
+ return discussion::from_record($discussionrecord);
+ }
+
+ /**
+ * Checks if a post exists.
+ * @param int $postid
+ * @return object $post
+ */
+ private function check_post_exists($postid) {
+ global $DB;
+ if (!$postrecord = $DB->get_record('moodleoverflow_posts', ['id' => $postid])) {
+ throw new moodle_exception('invalidpostid', 'moodleoverflow');
+ }
+ return post::from_record($postrecord);
+ }
+
+ // Capability checks.
+
+ /**
+ * Checks if a user can create a discussion.
+ * @return true
+ * @throws moodle_exception
+ */
+ private function check_user_can_create_discussion() {
+ if (!has_capability('mod/moodleoverflow:startdiscussion', $this->info->modulecontext)) {
+ throw new moodle_exception('cannotcreatediscussion', 'moodleoverflow');
+ }
+ return true;
+ }
+
+ /**
+ * Checks if a user can reply in a discussion.
+ * @return true
+ * @throws moodle_exception
+ */
+ private function check_user_can_create_reply() {
+ if (!has_capability('mod/moodleoverflow:replypost', $this->info->modulecontext, $this->prepost->userid)) {
+ throw new moodle_exception('cannotreply', 'moodleoverflow');
+ }
+ return true;
+ }
+
+ /**
+ * Checks if a user can edit a post.
+ * A user can edit if he can edit any post of if he edits his own post and has the ability to:
+ * start a new discussion or to reply to a post.
+ *
+ * @return true
+ * @throws moodle_exception
+ */
+ private function check_user_can_edit_post() {
+ global $USER;
+ $editanypost = has_capability('mod/moodleoverflow:editanypost', $this->info->modulecontext);
+ $replypost = has_capability('mod/moodleoverflow:replypost', $this->info->modulecontext);
+ $startdiscussion = has_capability('mod/moodleoverflow:startdiscussion', $this->info->modulecontext);
+ $ownpost = ($this->prepost->userid == $USER->id);
+ if (!(($ownpost && ($replypost || $startdiscussion)) || $editanypost)) {
+ throw new moodle_exception('cannotupdatepost', 'moodleoverflow');
+ }
+ return true;
+ }
+
+ /**
+ * Checks if a user can edit a post.
+ * @return true
+ * @throws moodle_exception
+ */
+ private function check_user_can_delete_post() {
+ global $USER;
+ $this->info->deleteownpost = has_capability('mod/moodleoverflow:deleteownpost', $this->info->modulecontext);
+ $this->info->deleteanypost = has_capability('mod/moodleoverflow:deleteanypost', $this->info->modulecontext);
+ if (!(($this->info->relatedpost->get_userid() == $USER->id && $this->info->deleteownpost) || $this->info->deleteanypost)) {
+
+ throw new moodle_exception('cannotdeletepost', 'moodleoverflow');
+ }
+ return true;
+ }
+}
diff --git a/classes/post_form.php b/classes/post_form.php
index 06cd80f017..2e1c66e2b5 100644
--- a/classes/post_form.php
+++ b/classes/post_form.php
@@ -43,8 +43,9 @@ class mod_moodleoverflow_post_form extends moodleform {
*/
public function definition() {
- $modform =& $this->_form;
+ $modform =& $this->_form;
$post = $this->_customdata['post'];
+ $edit = $this->_customdata['edit'];
$modcontext = $this->_customdata['modulecontext'];
$moodleoverflow = $this->_customdata['moodleoverflow'];
@@ -71,7 +72,7 @@ public function definition() {
}
// Submit buttons.
- if (isset($post->edit)) {
+ if ($edit) {
$strsubmit = get_string('savechanges');
} else {
$strsubmit = get_string('posttomoodleoverflow', 'moodleoverflow');
@@ -162,10 +163,3 @@ public static function editor_options(context_module $context, $postid) {
];
}
}
-
-
-
-
-
-
-
diff --git a/classes/review.php b/classes/review.php
index a0a86a7d4d..a566e9896d 100644
--- a/classes/review.php
+++ b/classes/review.php
@@ -134,7 +134,7 @@ public static function get_first_review_post($moodleoverflowid, $afterpostid = n
*/
public static function should_post_be_reviewed($post, $moodleoverflow): bool {
$reviewlevel = self::get_review_level($moodleoverflow);
- if ($post->parent) {
+ if ($post->parent != 0) {
return $reviewlevel == self::EVERYTHING;
} else {
return $reviewlevel >= self::QUESTIONS;
diff --git a/classes/subscriptions.php b/classes/subscriptions.php
index 2063c067f1..14fe64ceb7 100644
--- a/classes/subscriptions.php
+++ b/classes/subscriptions.php
@@ -26,6 +26,9 @@
namespace mod_moodleoverflow;
+use context_module;
+use stdClass;
+
/**
* Moodleoverflow subscription manager.
*
@@ -356,7 +359,7 @@ public static function subscription_disabled($moodleoverflow) {
* Checks wheter the specified moodleoverflow can be subscribed to.
*
* @param object $moodleoverflow The moodleoverflow ID
- * @param \context_module $context The module context.
+ * @param context_module $context The module context.
*
* @return boolean
*/
@@ -456,7 +459,7 @@ public static function get_unsubscribable_moodleoverflows() {
/**
* Get the list of potential subscribers to a moodleoverflow.
*
- * @param \context_module $context The moodleoverflow context.
+ * @param context_module $context The moodleoverflow context.
* @param string $fields The list of fields to return for each user.
* @param string $sort Sort order.
*
@@ -522,7 +525,7 @@ public static function fill_subscription_cache_for_course($courseid, $userid) {
* Returns a list of user object who are subscribed to this moodleoverflow.
*
* @param stdClass $moodleoverflow The moodleoverflow record
- * @param \context_module $context The moodleoverflow context
+ * @param context_module $context The moodleoverflow context
* @param string $fields Requested user fields
* @param boolean $includediscussions Whether to take discussion subscriptions into consideration
*
@@ -641,8 +644,8 @@ public static function reset_moodleoverflow_cache() {
* Adds user to the subscriber list.
*
* @param int $userid The user ID
- * @param \stdClass $moodleoverflow The moodleoverflow record
- * @param \context_module $context The module context
+ * @param stdClass $moodleoverflow The moodleoverflow record
+ * @param context_module $context The module context
* @param bool $userrequest Whether the user requested this change themselves.
*
* @return bool|int Returns true if the user is already subscribed or the subscription id if successfully subscribed.
@@ -656,7 +659,7 @@ public static function subscribe_user($userid, $moodleoverflow, $context, $userr
}
// Create a new subscription object.
- $sub = new \stdClass();
+ $sub = new stdClass();
$sub->userid = $userid;
$sub->moodleoverflow = $moodleoverflow->id;
@@ -706,8 +709,8 @@ public static function subscribe_user($userid, $moodleoverflow, $context, $userr
* Removes user from the subscriber list.
*
* @param int $userid The user ID.
- * @param \stdClass $moodleoverflow The moodleoverflow record
- * @param \context_module $context The module context
+ * @param stdClass $moodleoverflow The moodleoverflow record
+ * @param context_module $context The module context
* @param boolean $userrequest Whether the user requested this change themselves.
*
* @return bool Always returns true
@@ -761,9 +764,10 @@ public static function unsubscribe_user($userid, $moodleoverflow, $context, $use
/**
* Subscribes the user to the specified discussion.
*
+ * LEARNWEB-TODO: Refactor this function to the new way of working with discussion and posts.
* @param int $userid The user ID
- * @param \stdClass $discussion The discussion record
- * @param \context_module $context The module context
+ * @param stdClass $discussion The discussion record
+ * @param context_module $context The module context
*
* @return bool Whether a change was made
*/
@@ -807,7 +811,7 @@ public static function subscribe_user_to_discussion($userid, $discussion, $conte
} else {
// Else a new record needs to be created.
- $subscription = new \stdClass();
+ $subscription = new stdClass();
$subscription->userid = $userid;
$subscription->moodleoverflow = $discussion->moodleoverflow;
$subscription->discussion = $discussion->id;
@@ -836,9 +840,9 @@ public static function subscribe_user_to_discussion($userid, $discussion, $conte
/**
* Unsubscribes the user from the specified discussion.
*
- * @param int $userid The user ID
- * @param \stdClass $discussion The discussion record
- * @param \context_module $context The context module
+ * @param int $userid The user ID
+ * @param stdClass $discussion The discussion record
+ * @param context_module $context The context module
*
* @return bool Whether a change was made
*/
@@ -886,7 +890,7 @@ public static function unsubscribe_user_from_discussion($userid, $discussion, $c
// There is no record.
// Create a new discussion subscription record.
- $subscription = new \stdClass();
+ $subscription = new stdClass();
$subscription->userid = $userid;
$subscription->moodleoverflow = $discussion->moodleoverflow;
$subscription->discussion = $discussion->id;
@@ -985,13 +989,15 @@ public static function moodleoverflow_get_subscribe_link($moodleoverflow, $conte
/**
* Given a new post, subscribes the user to the thread the post was posted in.
*
- * @param \stdClass $moodleoverflow The moodleoverflow record
- * @param \stdClass $discussion The discussion record
- * @param \context_module $modulecontext The context of the module
+ * LEARNWEB-TODO: Refactor this function to the new way of working with discussion and posts.
+ * @param object $fromform The submitted form
+ * @param stdClass $moodleoverflow The moodleoverflow record
+ * @param stdClass $discussion The discussion record
+ * @param context_module $modulecontext The context of the module
*
* @return bool
*/
- public static function moodleoverflow_post_subscription($moodleoverflow, $discussion, $modulecontext) {
+ public static function moodleoverflow_post_subscription($fromform, $moodleoverflow, $discussion, $modulecontext) {
global $USER;
// Check for some basic information.
diff --git a/classes/tables/userstats_table.php b/classes/tables/userstats_table.php
index ad663e6bc9..dc2db1e610 100644
--- a/classes/tables/userstats_table.php
+++ b/classes/tables/userstats_table.php
@@ -30,6 +30,7 @@
require_once($CFG->dirroot . '/mod/moodleoverflow/lib.php');
require_once($CFG->dirroot . '/mod/moodleoverflow/locallib.php');
require_once($CFG->libdir . '/tablelib.php');
+use mod_moodleoverflow\output\helpicon;
/**
* Table listing all user statistics of a course
@@ -151,27 +152,11 @@ public function get_usertable() {
* Setup the help icon for amount of activity
*/
public function set_helpactivity() {
- global $CFG;
+ $htmlclass = 'helpactivityclass btn btn-link';
+ $content = get_string('helpamountofactivity', 'moodleoverflow');
+ $helpobject = new helpicon($htmlclass, $content);
$this->helpactivity = new \stdClass();
- $this->helpactivity->iconurl = $CFG->wwwroot . '/pix/a/help.png';
- $this->helpactivity->icon = \html_writer::img($this->helpactivity->iconurl,
- get_string('helpamountofactivity', 'moodleoverflow'));
- $this->helpactivity->class = 'helpactivityclass btn btn-link';
- $this->helpactivity->iconattributes = ['role' => 'button',
- 'data-container' => 'body',
- 'data-toggle' => 'popover',
- 'data-placement' => 'right',
- 'data-action' => 'showhelpicon',
- 'data-html' => 'true',
- 'data-trigger' => 'focus',
- 'tabindex' => '0',
- 'data-content' => '
';
echo $OUTPUT->footer();
diff --git a/externallib.php b/externallib.php
index 8e148d368f..98d8739e3b 100644
--- a/externallib.php
+++ b/externallib.php
@@ -22,6 +22,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+use mod_moodleoverflow\anonymous;
use mod_moodleoverflow\output\moodleoverflow_email;
use mod_moodleoverflow\review;
@@ -39,6 +40,7 @@
*/
class mod_moodleoverflow_external extends external_api {
+ // LEARNWEB-TODO: Adapt the functions to the new way of working with posts.
/**
* Returns description of method parameters
* @return external_function_parameters
@@ -54,7 +56,7 @@ public static function record_vote_parameters() {
/**
* Returns the result of the vote (new rating and reputations).
- * @return external_multiple_structure
+ * @return external_single_structure
*/
public static function record_vote_returns() {
return new external_single_structure(
@@ -121,7 +123,7 @@ public static function record_vote($postid, $ratingid) {
$ownerrating = \mod_moodleoverflow\ratings::moodleoverflow_get_reputation($moodleoverflow->id, $postownerid);
$raterrating = \mod_moodleoverflow\ratings::moodleoverflow_get_reputation($moodleoverflow->id, $USER->id);
- $cannotseeowner = \mod_moodleoverflow\anonymous::is_post_anonymous($discussion, $moodleoverflow, $USER->id) &&
+ $cannotseeowner = anonymous::is_post_anonymous($discussion, $moodleoverflow, $USER->id) &&
$USER->id != $postownerid;
$params['postrating'] = $rating->upvotes - $rating->downvotes;
@@ -254,6 +256,7 @@ public static function review_reject_post($postid, $reason = null) {
$renderertext = $PAGE->get_renderer('mod_moodleoverflow', 'email', 'textemail');
$userto = core_user::get_user($post->userid);
+ $userto->anonymous = anonymous::is_post_anonymous($discussion, $moodleoverflow, $post->userid);
$maildata = new moodleoverflow_email(
$course,
diff --git a/index.php b/index.php
index cac34f312a..193d769764 100644
--- a/index.php
+++ b/index.php
@@ -25,6 +25,8 @@
require_once(dirname(dirname(dirname(__FILE__))) . '/config.php');
global $CFG, $DB, $PAGE, $USER, $SESSION, $OUTPUT;
require_once(dirname(__FILE__) . '/locallib.php');
+
+global $CFG, $USER, $DB, $PAGE, $SESSION, $OUTPUT;
require_once($CFG->dirroot . '/course/lib.php');
// Require needed files.
@@ -56,9 +58,7 @@
unset($SESSION->fromdiscussion);
// Trigger the course module instace lise viewed evewnt.
-$params = [
- 'context' => context_course::instance($course->id),
-];
+$params = ['context' => context_course::instance($course->id)];
$event = \mod_moodleoverflow\event\course_module_instance_list_viewed::create($params);
$event->add_record_snapshot('course', $course);
$event->trigger();
diff --git a/lang/en/moodleoverflow.php b/lang/en/moodleoverflow.php
index f2c65c5235..169b594b81 100644
--- a/lang/en/moodleoverflow.php
+++ b/lang/en/moodleoverflow.php
@@ -172,7 +172,21 @@
$string['invalidpostid'] = 'Invalid post ID - {$a}';
$string['invalidratingid'] = 'The submitted rating is neither an upvote nor a downvote.';
$string['jump_to_next_post_needing_review'] = 'Jump to next post needing to be reviewed.';
+$string['la_endtime'] = 'Time at which students can no longer answer';
+$string['la_endtime_help'] = 'Students can not answer to qustions after the set up date';
+$string['la_endtime_ruleerror'] = 'End time must be in the future';
+$string['la_sequence_error'] = 'The end time must be after the start time';
+$string['la_starttime'] = 'Time at which students can start to answer';
+$string['la_starttime_help'] = 'Students can not answer to questions until the set up date';
+$string['la_starttime_ruleerror'] = 'Start time must be in the future';
$string['lastpost'] = 'Last post';
+$string['limitedanswer_helpicon_teacher'] = 'This can be changed in the settings of the Moodleoverflow.';
+$string['limitedanswer_info_endtime'] = 'Posts can not be answered after {$a->limitedanswerdate}.';
+$string['limitedanswer_info_start'] = 'This Moodleoverflow is in a limited answer mode.';
+$string['limitedanswer_info_starttime'] = 'Posts can not be answered until {$a->limitedanswerdate}.';
+$string['limitedanswerheading'] = 'Limited Answer Mode';
+$string['limitedanswerwarning_answers'] = 'There are already answered posts in this Moodleoverflow.';
+$string['limitedanswerwarning_conclusion'] = 'You can only set a time until students are able to answer';
$string['mailindexlink'] = 'Change your forum preferences: {$a}';
$string['manydiscussions'] = 'Discussions per page';
$string['markallread'] = 'Mark all posts in this discussion as read';
@@ -257,9 +271,7 @@
$string['postaddedtimeleft'] = 'You have {$a} to edit it if you want to make any changes.';
$string['postbyuser'] = '{$a->post} by {$a->user}';
$string['postincontext'] = 'See this post in context';
-$string['postmailinfolink'] = 'This is a copy of a message posted in {$a->coursename}.
-
-To reply click on this link: {$a->replylink}';
+$string['postmailinfolink'] = 'This is a copy of a message posted in {$a->coursename}. To reply click on this link: {$a->replylink}';
$string['postmailsubject'] = '{$a->courseshortname}: {$a->subject}';
$string['postnotexist'] = 'Requested post does not exist';
$string['posts'] = 'Posts';
diff --git a/lib.php b/lib.php
index eb80c2df4a..d8c512e6be 100644
--- a/lib.php
+++ b/lib.php
@@ -29,6 +29,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+// LEARNWEB-TODO: Adapt functions to the new way of working with posts and discussions (Replace the post/discussion functions).
use core\context\course;
defined('MOODLE_INTERNAL') || die();
@@ -343,7 +344,7 @@ function moodleoverflow_print_recent_activity($course, $viewfullnames, $timestar
/**
* Returns all other caps used in the module.
*
- * For example, this could be array('moodle/site:accessallgroups') if the
+ * For example, this could be ['moodle/site:accessallgroups'] if the
* module uses that capability.
*
* @return array
@@ -820,10 +821,12 @@ function moodleoverflow_send_mails() {
if (\mod_moodleoverflow\anonymous::is_post_anonymous($discussion, $moodleoverflow, $post->userid)) {
$userfrom = \core_user::get_noreply_user();
+ $userfrom->anonymous = true;
} else {
// Check whether the sending user is cached already.
if (array_key_exists($post->userid, $users)) {
$userfrom = $users[$post->userid];
+ $userfrom->anonymous = false;
} else {
// We dont know the the user yet.
@@ -831,6 +834,7 @@ function moodleoverflow_send_mails() {
$userfrom = $DB->get_record('user', ['id' => $post->userid]);
if ($userfrom) {
moodleoverflow_minimise_user_record($userfrom);
+ $userfrom->anonymous = false;
} else {
$uid = $post->userid;
$pid = $post->id;
@@ -1093,7 +1097,7 @@ function moodleoverflow_mark_old_posts_as_mailed($endtime) {
*
* @param stdClass $user
*/
-function moodleoverflow_minimise_user_record(stdClass $user) {
+function moodleoverflow_minimise_user_record(stdClass &$user) {
// Remove all information for the mail generation that are not needed.
unset($user->institution);
diff --git a/locallib.php b/locallib.php
index 375da72dfd..44b3a381d5 100644
--- a/locallib.php
+++ b/locallib.php
@@ -27,6 +27,8 @@
use mod_moodleoverflow\anonymous;
use mod_moodleoverflow\capabilities;
use mod_moodleoverflow\event\post_deleted;
+use mod_moodleoverflow\output\helpicon;
+use mod_moodleoverflow\ratings;
use mod_moodleoverflow\readtracking;
use mod_moodleoverflow\review;
@@ -47,6 +49,8 @@
function moodleoverflow_get_discussions($cm, $page = -1, $perpage = 0) {
global $DB, $CFG, $USER;
+ // LEARNWEB-TODO Refactor variable naming. $discussion->id is first post and $discussion->discussion is discussion id?
+
// User must have the permission to view the discussions.
$modcontext = context_module::instance($cm->id);
if (!capabilities::has(capabilities::VIEW_DISCUSSION, $modcontext)) {
@@ -255,7 +259,7 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = -
}
// Check if the question owner marked the question as helpful.
- $markedhelpful = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->discussion, false);
+ $markedhelpful = ratings::moodleoverflow_discussion_is_solved($discussion->discussion, false);
$preparedarray[$i]['starterlink'] = null;
if ($markedhelpful) {
$link = '/mod/moodleoverflow/discussion.php?d=';
@@ -266,7 +270,7 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = -
}
// Check if a teacher marked a post as solved.
- $markedsolution = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->discussion, true);
+ $markedsolution = ratings::moodleoverflow_discussion_is_solved($discussion->discussion, true);
$preparedarray[$i]['teacherlink'] = null;
if ($markedsolution) {
$link = '/mod/moodleoverflow/discussion.php?d=';
@@ -285,7 +289,7 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = -
}
// Get the amount of votes for the discussion.
- $votes = \mod_moodleoverflow\ratings::moodleoverflow_get_ratings_by_discussion($discussion->discussion, $discussion->id);
+ $votes = ratings::moodleoverflow_get_ratings_by_discussion($discussion->discussion, $discussion->id);
$votes = $votes->upvotes - $votes->downvotes;
$preparedarray[$i]['votetext'] = ($votes == 1) ? 'vote' : 'votes';
@@ -397,13 +401,13 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = -
$preparedarray[$i]['votes'] = $votes;
// Did the user rated this post?
- $rating = \mod_moodleoverflow\ratings::moodleoverflow_user_rated($discussion->firstpost);
+ $rating = ratings::moodleoverflow_user_rated($discussion->firstpost);
$firstpost = moodleoverflow_get_post_full($discussion->firstpost);
$preparedarray[$i]['userupvoted'] = ($rating->rating ?? null) == RATING_UPVOTE;
$preparedarray[$i]['userdownvoted'] = ($rating->rating ?? null) == RATING_DOWNVOTE;
- $preparedarray[$i]['canchange'] = \mod_moodleoverflow\ratings::moodleoverflow_user_can_rate($firstpost, $context) &&
+ $preparedarray[$i]['canchange'] = ratings::moodleoverflow_user_can_rate($firstpost, $context) &&
$startuser->id != $USER->id;
$preparedarray[$i]['postid'] = $discussion->firstpost;
@@ -527,6 +531,7 @@ function moodleoverflow_count_discussion_replies($cm) {
}
/**
+ * LEARNWEB-TODO: Delete this function when adapting the print-functions to the new post and discussion structure.
* Check if the user is capable of starting a new discussion.
*
* @param object $moodleoverflow
@@ -537,7 +542,7 @@ function moodleoverflow_count_discussion_replies($cm) {
*/
function moodleoverflow_user_can_post_discussion($moodleoverflow, $cm = null, $context = null) {
- // Guests an not-logged-in users can not psot.
+ // Guests an not-logged-in users can not post.
if (isguestuser() || !isloggedin()) {
return false;
}
@@ -653,13 +658,8 @@ function moodleoverflow_get_discussions_unread($cm) {
* @return mixed array of posts or false
*/
function moodleoverflow_get_post_full($postid) {
- global $DB, $CFG;
-
- if ($CFG->branch >= 311) {
- $allnames = \core_user\fields::for_name()->get_sql('u', false, '', '', false)->selects;
- } else {
- $allnames = get_all_user_name_fields(true, 'u');
- }
+ global $DB;
+ $allnames = \core_user\fields::for_name()->get_sql('u', false, '', '', false)->selects;
$sql = "SELECT p.*, d.moodleoverflow, $allnames, u.email, u.picture, u.imagealt
FROM {moodleoverflow_posts} p
JOIN {moodleoverflow_discussions} d ON p.discussion = d.id
@@ -925,16 +925,19 @@ function moodleoverflow_user_can_post($modulecontext, $posttoreplyto, $considerr
/**
* Prints a moodleoverflow discussion.
+ * LEARNWEB-TODO: REFACTOR WITH NEW POST AND DISCUSSION STRUCTURE.
*
- * @param stdClass $course The course object
+ * @param stdClass $course The course object
* @param object $cm
- * @param stdClass $moodleoverflow The moodleoverflow object
- * @param stdClass $discussion The discussion object
- * @param stdClass $post The post object
- * @param bool $multiplemarks The setting of multiplemarks (default: multiplemarks are not allowed)
+ * @param stdClass $moodleoverflow The moodleoverflow object
+ * @param stdClass $discussion The discussion object
+ * @param stdClass $post The post object
+ * @param bool $multiplemarks The setting of multiplemarks (default: multiplemarks are not allowed)
+ * @param stdClass|null $limitedanswersetting Two Unix timestamp wrapped in a stdClass, upper and lower label for answering.
*/
-function moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discussion, $post, $multiplemarks = false) {
- global $USER;
+function moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discussion, $post,
+ $multiplemarks = false, ?stdClass $limitedanswersetting = null) {
+ global $USER, $DB;
// Check if the current is the starter of the discussion.
$ownpost = (isloggedin() && ($USER->id == $post->userid));
@@ -945,6 +948,7 @@ function moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discuss
$istracked = readtracking::moodleoverflow_is_tracked($moodleoverflow);
// Retrieve all posts of the discussion.
+ // This part is adapted/refactored to the new way of working with posts (use of get_id() function and discussion object).
$posts = moodleoverflow_get_all_discussion_posts($discussion->id, $istracked, $modulecontext);
$usermapping = anonymous::get_userid_mapping($moodleoverflow, $discussion->id);
@@ -985,7 +989,7 @@ function moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discuss
// Print the starting post.
echo moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $course,
- $ownpost, false, '', '', $postread, true, $istracked, 0, $usermapping, 0, $multiplemarks);
+ $ownpost, false, '', '', $postread, true, $istracked, 0, $usermapping, 0, $multiplemarks, $limitedanswersetting);
// Print answer divider.
if ($answercount == 1) {
@@ -999,7 +1003,7 @@ function moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discuss
// Print the other posts.
echo moodleoverflow_print_posts_nested($course, $cm, $moodleoverflow, $discussion, $post, $istracked, $posts,
- null, $usermapping, $multiplemarks);
+ null, $usermapping, $multiplemarks, $limitedanswersetting);
echo '';
}
@@ -1060,7 +1064,7 @@ function moodleoverflow_get_all_discussion_posts($discussionid, $tracking, $modc
}
// Load all ratings.
- $discussionratings = \mod_moodleoverflow\ratings::moodleoverflow_get_ratings_by_discussion($discussionid);
+ $discussionratings = ratings::moodleoverflow_get_ratings_by_discussion($discussionid);
// Assign ratings to the posts.
foreach ($posts as $postid => $post) {
@@ -1074,7 +1078,7 @@ function moodleoverflow_get_all_discussion_posts($discussionid, $tracking, $modc
}
// Order the answers by their ratings.
- $posts = \mod_moodleoverflow\ratings::moodleoverflow_sort_answers_by_ratings($posts);
+ $posts = ratings::moodleoverflow_sort_answers_by_ratings($posts);
// Find all children of this post.
foreach ($posts as $postid => $post) {
@@ -1112,6 +1116,8 @@ function moodleoverflow_get_all_discussion_posts($discussionid, $tracking, $modc
/**
+ *
+ * LEARNWEB-TODO: REFACTOR THIS FUNCTION FOR THE NEW POST STRUCTURE.
* Prints a moodleoverflow post.
* @param object $post
* @param object $discussion
@@ -1129,6 +1135,7 @@ function moodleoverflow_get_all_discussion_posts($discussionid, $tracking, $modc
* @param array $usermapping
* @param int $level
* @param bool $multiplemarks setting of multiplemarks
+ * @param stdClass|null $limitedanswersetting
* @return void|null
* @throws coding_exception
* @throws dml_exception
@@ -1138,8 +1145,9 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co
$ownpost = false, $link = false,
$footer = '', $highlight = '', $postisread = null,
$dummyifcantsee = true, $istracked = false,
- $iscomment = false, $usermapping = [], $level = 0, $multiplemarks = false) {
- global $USER, $CFG, $OUTPUT, $PAGE;
+ $iscomment = false, $usermapping = [], $level = 0,
+ $multiplemarks = false, ?stdClass $limitedanswersetting = null) {
+ global $USER, $CFG, $OUTPUT, $PAGE, $DB;
// Require the filelib.
require_once($CFG->libdir . '/filelib.php');
@@ -1162,6 +1170,7 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co
$post->course = $course->id;
$post->moodleoverflow = $moodleoverflow->id;
$mcid = $modulecontext->id;
+ $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $mcid, 'mod_moodleoverflow', 'post', $post->id);
// Check if the user has the capability to see posts.
if (!moodleoverflow_user_can_see_post($moodleoverflow, $discussion, $post, $cm)) {
@@ -1240,8 +1249,8 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co
$helpfulposts = false;
$solvedposts = false;
if ($multiplemarks) {
- $helpfulposts = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->id, false);
- $solvedposts = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->id, true);
+ $helpfulposts = ratings::moodleoverflow_discussion_is_solved($discussion->id, false);
+ $solvedposts = ratings::moodleoverflow_discussion_is_solved($discussion->id, true);
}
// If the user has started the discussion, he can mark the answer as helpful.
@@ -1310,16 +1319,34 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co
// Give the option to reply to a post.
if (moodleoverflow_user_can_post($modulecontext, $post, false)) {
-
$attributes = [
'class' => 'onlyifreviewed',
];
-
// Answer to the parent post.
if (empty($post->parent)) {
- $replyurl = new moodle_url('/mod/moodleoverflow/post.php#mformmoodleoverflow', ['reply' => $post->id]);
- $commands[] = ['url' => $replyurl, 'text' => $str->replyfirst, 'attributes' => $attributes];
-
+ // Check if limitedanswertime is on.
+ $settingexist = $limitedanswersetting->la_starttime != 0 || $limitedanswersetting->la_endtime != 0;
+ if ($settingexist) {
+ $infolimited = $limitedanswersetting->la_starttime ? " " . get_string('limitedanswer_info_starttime',
+ 'moodleoverflow', ['limitedanswerdate' => date('d.m.Y H:i', $limitedanswersetting->la_starttime)]) : '';
+ $infolimited .= $limitedanswersetting->la_endtime ? " " . get_string('limitedanswer_info_endtime', 'moodleoverflow',
+ ['limitedanswerdate' => date('d.m.Y H:i', $limitedanswersetting->la_endtime)]) : '';
+ echo html_writer::div($infolimited, 'alert alert-warning', ['role' => 'alert']);
+ }
+ if (is_currently_time_limited($limitedanswersetting)) {
+ if (!has_capability('mod/moodleoverflow:addinstance', $modulecontext)) {
+ // In case the user can not change the limited answer time he/she can not answer.
+ render_limited_answer('text-muted', $commands, $infolimited, 'student', $str->replyfirst);
+ } else {
+ // The user is a teacher.
+ $replyurl = new moodle_url('/mod/moodleoverflow/post.php#mformmoodleoverflow', ['reply' => $post->id]);
+ $answerbutton = html_writer::link($replyurl, $str->replyfirst, ['class' => 'onlyifreviewed answerbutton']);
+ render_limited_answer('', $commands, $infolimited, 'teacher', $answerbutton);
+ }
+ } else {
+ $replyurl = new moodle_url('/mod/moodleoverflow/post.php#mformmoodleoverflow', ['reply' => $post->id]);
+ $commands[] = ['url' => $replyurl, 'text' => $str->replyfirst, 'attributes' => $attributes];
+ }
// If the post is a comment, answer to the parent post.
} else if (!$iscomment) {
$replyurl = new moodle_url('/mod/moodleoverflow/post.php#mformmoodleoverflow', ['reply' => $post->id]);
@@ -1349,7 +1376,7 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co
$mustachedata->markedsolution = $post->markedsolution;
// Did the user rated this post?
- $rating = \mod_moodleoverflow\ratings::moodleoverflow_user_rated($post->id);
+ $rating = ratings::moodleoverflow_user_rated($post->id);
// Initiate the variables.
$mustachedata->userupvoted = false;
@@ -1423,7 +1450,7 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co
if (anonymous::is_post_anonymous($discussion, $moodleoverflow, $post->userid)) {
$postuserrating = null;
} else {
- $postuserrating = \mod_moodleoverflow\ratings::moodleoverflow_get_reputation($moodleoverflow->id, $postinguser->id);
+ $postuserrating = ratings::moodleoverflow_get_reputation($moodleoverflow->id, $postinguser->id);
}
// The name of the user and the date modified.
@@ -1470,7 +1497,11 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co
$commandhtml = [];
foreach ($commands as $command) {
if (is_array($command)) {
- $commandhtml[] = html_writer::link($command['url'], $command['text'], $command['attributes'] ?? null);
+ if (array_key_exists('limitedanswer', $command)) {
+ $commandhtml[] = html_writer::tag('span', $command['text'], $command['attributes'] ?? null);
+ } else {
+ $commandhtml[] = html_writer::link($command['url'], $command['text'], $command['attributes'] ?? null);
+ }
} else {
$commandhtml[] = $command;
}
@@ -1494,27 +1525,66 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co
return $renderer->render_post($mustachedata);
}
+/**
+ * Check if the limited answer setting is currently disabling answers.
+ * @param stdClass $limitedanswersetting Two Unix timestamp wrapped in a stdClass, upper and lower label for answering.
+ * @return bool
+ */
+function is_currently_time_limited($limitedanswersetting): bool {
+ return ($limitedanswersetting->la_starttime != 0 && $limitedanswersetting->la_starttime > time())
+ || ($limitedanswersetting->la_endtime != 0 && $limitedanswersetting->la_endtime < time());
+}
+
+/**
+ * Renders the answer action in a post.
+ * @param String $htmlattributes additional attributes passed to the html class and the command (either 'text-muted' or empty).
+ * @param array $commands array of actions available to the user in a post.
+ * @param String $infolimited information about the limited answer setting.
+ * @param String $role either 'student' or 'teacher'.
+ * @param String $helpstring content for the tag specifing the helpicon for the answer button.
+ * @return void
+ * @throws coding_exception
+ */
+function render_limited_answer($htmlattributes, &$commands, $infolimited, $role, $helpstring) {
+ $limitedanswerattributes = ['class' => 'onlyifreviewed ' . $htmlattributes];
+ $htmlclass = 'onlyifreviewed helpicon ' . $htmlattributes;
+ $content = get_string('limitedanswer_info_start', 'moodleoverflow');
+ $content .= $infolimited;
+ $htmlattributes == '' ? $content .= " " . get_string('limitedanswer_helpicon_teacher', 'moodleoverflow') : $content .= '';
+
+ $helpobject = new helpicon($htmlclass, $content);
+ $helpicon = $helpobject->get_helpicon();
+ // Build a html span that has the answer button and the help icon.
+ $limitedanswerobject = html_writer::tag('span', $helpstring . ' ' . $helpicon);
+
+ // Save the span in the commands with an extra value.
+ $commands[] = ['text' => $limitedanswerobject,
+ 'attributes' => $limitedanswerattributes,
+ 'limitedanswer' => $role, ];
+}
/**
* Prints all posts of the discussion in a nested form.
*
- * @param object $course The course object
+ * @param object $course The course object
* @param object $cm
- * @param object $moodleoverflow The moodleoverflow object
- * @param object $discussion The discussion object
- * @param object $parent The object of the parent post
- * @param bool $istracked Whether the user tracks the discussion
- * @param array $posts Array of posts within the discussion
- * @param bool $iscomment Whether the current post is a comment
- * @param array $usermapping
- * @param bool $multiplemarks
+ * @param object $moodleoverflow The moodleoverflow object
+ * @param object $discussion The discussion object
+ * @param object $parent The object of the parent post
+ * @param bool $istracked Whether the user tracks the discussion
+ * @param array $posts Array of posts within the discussion
+ * @param bool $iscomment Whether the current post is a comment
+ * @param array $usermapping
+ * @param bool $multiplemarks The setting of multiplemarks (default: multiplemarks are not allowed)
+ * @param stdClass|null $limitedanswersetting Two Unix timestamp wrapped in a stdClass, upper and lower label for answering.
* @return string
* @throws coding_exception
* @throws dml_exception
* @throws moodle_exception
*/
function moodleoverflow_print_posts_nested($course, &$cm, $moodleoverflow, $discussion, $parent,
- $istracked, $posts, $iscomment = null, $usermapping = [], $multiplemarks = false) {
+ $istracked, $posts, $iscomment = null, $usermapping = [],
+ $multiplemarks = false, ?stdClass $limitedanswersetting = null) {
global $USER;
// Prepare the output.
@@ -1555,12 +1625,13 @@ function moodleoverflow_print_posts_nested($course, &$cm, $moodleoverflow, $disc
$postread = !empty($post->postread);
// Print the answer.
- $output .= moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $course,
- $ownpost, false, '', '', $postread, true, $istracked, $parentid, $usermapping, $level, $multiplemarks);
+ $output .= moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $course, $ownpost, false, '', '',
+ $postread, true, $istracked, $parentid, $usermapping, $level,
+ $multiplemarks, $limitedanswersetting);
// Print its children.
$output .= moodleoverflow_print_posts_nested($course, $cm, $moodleoverflow,
- $discussion, $post, $istracked, $posts, $parentid, $usermapping, $multiplemarks);
+ $discussion, $post, $istracked, $posts, $parentid, $usermapping, $multiplemarks, $limitedanswersetting);
// End the div.
$output .= "\n";
@@ -1572,6 +1643,7 @@ function moodleoverflow_print_posts_nested($course, &$cm, $moodleoverflow, $disc
}
/**
+ * LEARNWEB-TODO: Delete this function after adapting the print_post function to the new post structure
* Returns attachments with information for the template
*
* @param object $post
@@ -1653,6 +1725,7 @@ function moodleoverflow_add_attachment($post, $forum, $cm) {
}
/**
+ * WARNING: this function is only used in the lib.php. For other uses this function is deprecated.
* Adds a new post in an existing discussion.
* @param object $post The post object
* @return bool|int The Id of the post if operation was successful
@@ -1709,93 +1782,6 @@ function moodleoverflow_add_new_post($post) {
return $post->id;
}
-/**
- * Updates a specific post.
- *
- * Capabilities are not checked, because this is happening in the post.php.
- *
- * @param object $newpost The new post object
- *
- * @return bool Whether the update was successful
- */
-function moodleoverflow_update_post($newpost) {
- global $DB, $USER;
-
- // Retrieve not submitted variables.
- $post = $DB->get_record('moodleoverflow_posts', ['id' => $newpost->id]);
- $discussion = $DB->get_record('moodleoverflow_discussions', ['id' => $post->discussion]);
- $moodleoverflow = $DB->get_record('moodleoverflow', ['id' => $discussion->moodleoverflow]);
- $cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflow->id);
- $context = context_module::instance($cm->id);
-
- // Allowed modifiable fields.
- $modifiablefields = [
- 'message',
- 'messageformat',
- ];
-
- // Iteratate through all modifiable fields and update the values.
- foreach ($modifiablefields as $field) {
- if (isset($newpost->{$field})) {
- $post->{$field} = $newpost->{$field};
- }
- }
-
- $post->modified = time();
- if ($newpost->reviewed ?? $post->reviewed) {
- // Update the date and the user of the post and the discussion.
- $discussion->timemodified = $post->modified;
- $discussion->usermodified = $post->userid;
- }
-
- // When editing the starting post of a discussion.
- if (!$post->parent) {
- $discussion->name = $newpost->subject;
- }
-
- // Save draft files to permanent file area.
- $post->message = file_save_draft_area_files($newpost->draftideditor, $context->id, 'mod_moodleoverflow', 'post',
- $post->id, mod_forum_post_form::editor_options($context, $post->id), $post->message);
-
- // Update the post and the corresponding discussion.
- $DB->update_record('moodleoverflow_posts', $post);
- $DB->update_record('moodleoverflow_discussions', $discussion);
-
- $cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflow->id);
- moodleoverflow_add_attachment($newpost, $moodleoverflow, $cm);
-
- // Mark the edited post as read.
- $cantrack = readtracking::moodleoverflow_can_track_moodleoverflows($moodleoverflow);
- $istracked = readtracking::moodleoverflow_is_tracked($moodleoverflow);
- if ($cantrack && $istracked) {
- readtracking::moodleoverflow_mark_post_read($USER->id, $post);
- }
-
- // The post has been edited successfully.
- return true;
-}
-
-/**
- * Count all replies of a post.
- *
- * @param object $post The post object
- * @param bool $onlyreviewed Whether to count only reviewed posts.
- *
- * @return int Amount of replies
- */
-function moodleoverflow_count_replies($post, $onlyreviewed) {
- global $DB;
-
- $conditions = ['parent' => $post->id];
-
- if ($onlyreviewed) {
- $conditions['reviewed'] = '1';
- }
-
- // Return the amount of replies.
- return $DB->count_records('moodleoverflow_posts', $conditions);
-}
-
/**
* Deletes a discussion and handles all associated cleanups.
*
@@ -1924,6 +1910,7 @@ function moodleoverflow_delete_post($post, $deletechildren, $cm, $moodleoverflow
}
/**
+ * WARNING: this function is only used in the lib.php. For other uses this function is deprecated.
* Sets the last post for a given discussion.
*
* @param int $discussionid The discussion ID
@@ -2135,7 +2122,7 @@ function moodleoverflow_update_all_grades_for_cm($moodleoverflowid) {
}
// Get user reputation.
- $userrating = \mod_moodleoverflow\ratings::moodleoverflow_get_reputation($moodleoverflow->id, $userid, true);
+ $userrating = ratings::moodleoverflow_get_reputation($moodleoverflow->id, $userid, true);
// Calculate the posting user's updated grade.
moodleoverflow_update_user_grade_on_db($moodleoverflow, $userrating, $userid);
diff --git a/mod_form.php b/mod_form.php
index 30cdef4367..987b98f23f 100644
--- a/mod_form.php
+++ b/mod_form.php
@@ -45,7 +45,7 @@ class mod_moodleoverflow_mod_form extends moodleform_mod {
* Defines forms elements.
*/
public function definition() {
- global $CFG, $COURSE, $PAGE;
+ global $CFG, $COURSE, $PAGE, $DB;
// Define the modform.
$mform = $this->_form;
@@ -229,6 +229,45 @@ public function definition() {
$mform->addHelpButton('allowmultiplemarks', 'allowmultiplemarks', 'moodleoverflow');
$mform->setDefault('allowmultiplemarks', 0);
+ // Limited answer options.
+ $mform->addElement('header', 'limitedanswerheading', get_string('limitedanswerheading', 'moodleoverflow'));
+
+ $answersfound = false;
+ if (!empty($this->current->id)) {
+ // Check if there are already answered posts in this moodleoverflow and place a warning if so.
+ $sql = 'SELECT COUNT(*) AS answerposts
+ FROM {moodleoverflow_discussions} discuss JOIN {moodleoverflow_posts} posts
+ ON discuss.id = posts.discussion
+ WHERE posts.parent != 0
+ AND discuss.moodleoverflow = ' . $this->current->id . ';';
+ $answerpostscount = $DB->get_records_sql($sql);
+ $answerpostscount = $answerpostscount[array_key_first($answerpostscount)]->answerposts;
+ $answersfound = $answerpostscount > 0;
+ if ($answersfound) {
+ $warningstring = get_string('limitedanswerwarning_answers', 'moodleoverflow');
+ $warningstring .= ' ' . get_string('limitedanswerwarning_conclusion', 'moodleoverflow');
+ $htmlwarning = html_writer::div($warningstring, 'alert alert-warning', ['role' => 'alert']);
+ $mform->addElement('html', $htmlwarning);
+ }
+ }
+
+ // Limited answer setting elements..
+ $mform->addElement('hidden', 'la_answersfound', $answersfound);
+ $mform->setType('la_answersfound', PARAM_BOOL);
+ $mform->addElement('date_time_selector', 'la_starttime', get_string('la_starttime', 'moodleoverflow'),
+ ['optional' => true]);
+
+ $mform->addHelpButton('la_starttime', 'la_starttime', 'moodleoverflow');
+ $mform->disabledIf('la_starttime', 'la_answersfound', 'eq', true);
+
+ $mform->addElement('date_time_selector', 'la_endtime', get_string('la_endtime', 'moodleoverflow'),
+ ['optional' => true]);
+
+ $mform->addHelpButton('la_endtime', 'la_endtime', 'moodleoverflow');
+
+ $mform->addElement('hidden', 'la_error');
+ $mform->setType('la_error', PARAM_TEXT);
+
// Add standard elements, common to all modules.
$this->standard_coursemodule_elements();
@@ -249,4 +288,39 @@ public function data_postprocessing($data) {
$data->coursewidereputation = false;
}
}
+
+ /**
+ * Validates set data in mod_form
+ * @param array $data
+ * @param array $files
+ * @return array
+ * @throws coding_exception
+ */
+ public function validation($data, $files): array {
+ $errors = parent::validation($data, $files);
+
+ // Validate that the limited answer settings.
+ $currenttime = time();
+ $isstarttime = !empty($data['la_starttime']);
+ $isendtime = !empty($data['la_endtime']);
+
+ if ($isstarttime && $data['la_starttime'] < $currenttime) {
+ $errors['la_starttime'] = get_string('la_starttime_ruleerror', 'moodleoverflow');
+ }
+ if ($isendtime) {
+ if ($data['la_endtime'] < $currenttime) {
+ $errors['la_endtime'] = get_string('la_endtime_ruleerror', 'moodleoverflow');
+ }
+
+ if ($isstarttime && $data['la_endtime'] <= $data['la_starttime']) {
+ if (isset($errors['la_endtime'])) {
+ $errors['la_endtime'] .= ' ' . get_string('la_sequence_error', 'moodleoverflow');
+ } else {
+ $errors['la_endtime'] = get_string('la_sequence_error', 'moodleoverflow');
+ }
+ }
+ }
+
+ return $errors;
+ }
}
diff --git a/pix/monologo.svg b/pix/monologo.svg
index a6ea2dcfcd..66041b0217 100644
--- a/pix/monologo.svg
+++ b/pix/monologo.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/post.php b/post.php
index 86bf67b23c..43caa1e6db 100644
--- a/post.php
+++ b/post.php
@@ -15,13 +15,14 @@
// along with Moodle. If not, see .
/**
- * The file to manage posts.
+ * The file that is opened in Moodle when the user interacts with posts
*
* @package mod_moodleoverflow
- * @copyright 2017 Kennet Winter
+ * @copyright 2023 Tamaro Walter
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+use mod_moodleoverflow\post\post_control;
// Include config and locallib.
use mod_moodleoverflow\anonymous;
use mod_moodleoverflow\review;
@@ -31,31 +32,16 @@
require_once(dirname(__FILE__) . '/locallib.php');
require_once($CFG->libdir . '/completionlib.php');
-// Declare optional parameters.
+// Declare optional url parameters.
$moodleoverflow = optional_param('moodleoverflow', 0, PARAM_INT);
$reply = optional_param('reply', 0, PARAM_INT);
$edit = optional_param('edit', 0, PARAM_INT);
$delete = optional_param('delete', 0, PARAM_INT);
$confirm = optional_param('confirm', 0, PARAM_INT);
-$count = 0;
-$count += $moodleoverflow ? 1 : 0;
-$count += $reply ? 1 : 0;
-$count += $edit ? 1 : 0;
-$count += $delete ? 1 : 0;
-
-if ($count !== 1) {
- throw new coding_exception('Exactly one parameter should be specified!');
-}
-
// Set the URL that should be used to return to this page.
-$PAGE->set_url('/mod/moodleoverflow/post.php', [
- 'moodleoverflow' => $moodleoverflow,
- 'reply' => $reply,
- 'edit' => $edit,
- 'delete' => $delete,
- 'confirm' => $confirm,
-]);
+$PAGE->set_url('/mod/moodleoverflow/post.php', ['moodleoverflow' => $moodleoverflow, 'reply' => $reply, 'edit' => $edit,
+ 'delete' => $delete, 'confirm' => $confirm, ]);
// These params will be passed as hidden variables later in the form.
$pageparams = ['moodleoverflow' => $moodleoverflow, 'reply' => $reply, 'edit' => $edit];
@@ -63,739 +49,105 @@
// Get the system context instance.
$systemcontext = context_system::instance();
-// Catch guests.
-if (!isloggedin() || isguestuser()) {
-
- // The user is starting a new discussion in a moodleoverflow instance.
- if (!empty($moodleoverflow)) {
-
- // Check the moodleoverflow instance is valid.
- if (!$moodleoverflow = $DB->get_record('moodleoverflow', ['id' => $moodleoverflow])) {
- throw new moodle_exception('invalidmoodleoverflowid', 'moodleoverflow');
- }
-
- // The user is replying to an existing moodleoverflow discussion.
- } else if (!empty($reply)) {
-
- // Check if the related post exists.
- if (!$parent = moodleoverflow_get_post_full($reply)) {
- throw new moodle_exception('invalidparentpostid', 'moodleoverflow');
- }
-
- // Check if the post is part of a valid discussion.
- if (!$discussion = $DB->get_record('moodleoverflow_discussions', ['id' => $parent->discussion])) {
- throw new moodle_exception('notpartofdiscussion', 'moodleoverflow');
- }
-
- // Check if the post is related to a valid moodleoverflow instance.
- if (!$moodleoverflow = $DB->get_record('moodleoverflow', ['id' => $discussion->moodleoverflow])) {
- throw new moodle_exception('invalidmoodleoverflowid', 'moodleoverflow');
- }
- }
-
- // Get the related course.
- if (!$course = $DB->get_record('course', ['id' => $moodleoverflow->course])) {
- throw new moodle_exception('invalidcourseid');
- }
-
- // Get the related coursemodule and its context.
- if (!$cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflow->id, $course->id)) {
- throw new moodle_exception('invalidcoursemodule');
- }
+// Create a post_control object to control and lead the process.
+$postcontrol = new post_control();
- // Get the context of the module.
- $modulecontext = context_module::instance($cm->id);
+// Put all interaction parameters in one object for the post_control.
+$urlparameter = new stdClass();
+$urlparameter->create = $moodleoverflow;
+$urlparameter->reply = $reply;
+$urlparameter->edit = $edit;
+$urlparameter->delete = $delete;
- // Set parameters for the page.
- $PAGE->set_cm($cm, $course, $moodleoverflow);
- $PAGE->set_context($modulecontext);
- $PAGE->set_title($course->shortname);
- $PAGE->set_heading($course->fullname);
-
- // The page should not be large, only pages containing broad tables are usually.
- $PAGE->add_body_class('limitedwidth');
+// Catch guests.
+if (!isloggedin() || isguestuser()) {
+ // Gather information and set the page right so that user can be redirected to the right site.
+ $information = $postcontrol->catch_guest();
// The guest needs to login.
- echo $OUTPUT->header();
$strlogin = get_string('noguestpost', 'forum') . '
' . get_string('liketologin');
- echo $OUTPUT->confirm($strlogin, get_login_url(), $CFG->wwwroot . '/mod/moodleoverflow/view.php?m=' . $moodleoverflow->id);
+ echo $OUTPUT->header();
+ echo $OUTPUT->confirm($strlogin, get_login_url(),
+ $CFG->wwwroot . '/mod/moodleoverflow/view.php?m= ' . $information->moodleoverflow->id);
echo $OUTPUT->footer();
exit;
}
-// First step: A general login is needed to post something.
+// Require a general login to post something.
+// LEARNWEB-TODO: should course or id really be zero?.
require_login(0, false);
-// First possibility: User is starting a new discussion in a moodleoverflow instance.
-if (!empty($moodleoverflow)) {
-
- // Check the moodleoverflow instance is valid.
- if (!$moodleoverflow = $DB->get_record('moodleoverflow', ['id' => $moodleoverflow])) {
- throw new moodle_exception('invalidmoodleoverflowid', 'moodleoverflow');
- }
-
- // Get the related course.
- if (!$course = $DB->get_record('course', ['id' => $moodleoverflow->course])) {
- throw new moodle_exception('invalidcourseid');
- }
-
- // Get the related coursemodule.
- if (!$cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflow->id, $course->id)) {
- throw new moodle_exception('invalidcoursemodule');
- }
-
- // Retrieve the contexts.
- $modulecontext = context_module::instance($cm->id);
- $coursecontext = context_course::instance($course->id);
-
- // Check if the user can start a new discussion.
- if (!moodleoverflow_user_can_post_discussion($moodleoverflow, $cm, $modulecontext)) {
-
- // Catch unenrolled user.
- if (!isguestuser() && !is_enrolled($coursecontext)) {
- if (enrol_selfenrol_available($course->id)) {
- $SESSION->wantsurl = qualified_me();
- $SESSION->enrolcancel = get_local_referer(false);
- redirect(new moodle_url('/enrol/index.php', [
- 'id' => $course->id,
- 'returnurl' => '/mod/moodleoverflow/view.php?m=' . $moodleoverflow->id,
- ]), get_string('youneedtoenrol'));
- }
- }
-
- // Notify the user, that he can not post a new discussion.
- throw new moodle_exception('nopostmoodleoverflow', 'moodleoverflow');
- }
-
- // Where is the user coming from?
- $SESSION->fromurl = get_local_referer(false);
-
- // Load all the $post variables.
- $post = new stdClass();
- $post->course = $course->id;
- $post->moodleoverflow = $moodleoverflow->id;
- $post->discussion = 0;
- $post->parent = 0;
- $post->subject = '';
- $post->userid = $USER->id;
- $post->message = '';
-
- // Unset where the user is coming from.
- // Allows to calculate the correct return url later.
- unset($SESSION->fromdiscussion);
-
-} else if (!empty($reply)) {
- // Second possibility: The user is writing a new reply.
-
- // Check if the post exists.
- if (!$parent = moodleoverflow_get_post_full($reply)) {
- throw new moodle_exception('invalidparentpostid', 'moodleoverflow');
- }
-
- // Check if the post is part of a discussion.
- if (!$discussion = $DB->get_record('moodleoverflow_discussions', ['id' => $parent->discussion])) {
- throw new moodle_exception('notpartofdiscussion', 'moodleoverflow');
- }
-
- // Check if the discussion is part of a moodleoverflow instance.
- if (!$moodleoverflow = $DB->get_record('moodleoverflow', ['id' => $discussion->moodleoverflow])) {
- throw new moodle_exception('invalidmoodleoverflowid', 'moodleoverflow');
- }
-
- // Check if the moodleoverflow instance is part of a course.
- if (!$course = $DB->get_record('course', ['id' => $discussion->course])) {
- throw new moodle_exception('invalidcourseid');
- }
-
- // Retrieve the related coursemodule.
- if (!$cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflow->id, $course->id)) {
- throw new moodle_exception('invalidcoursemodule');
- }
-
- // Ensure the coursemodule is set correctly.
- $PAGE->set_cm($cm, $course, $moodleoverflow);
-
- // Retrieve the other contexts.
- $modulecontext = context_module::instance($cm->id);
- $coursecontext = context_course::instance($course->id);
-
- // Check whether the user is allowed to post.
- if (!moodleoverflow_user_can_post($modulecontext, $parent)) {
-
- // Give the user the chance to enroll himself to the course.
- if (!isguestuser() && !is_enrolled($coursecontext)) {
- $SESSION->wantsurl = qualified_me();
- $SESSION->enrolcancel = get_local_referer(false);
- redirect(new moodle_url('/enrol/index.php',
- ['id' => $course->id, 'returnurl' => '/mod/moodleoverflow/view.php?m=' . $moodleoverflow->id]),
- get_string('youneedtoenrol'));
- }
-
- // Print the error message.
- throw new moodle_exception('nopostmoodleoverflow', 'moodleoverflow');
- }
-
- // Make sure the user can post here.
- if (!$cm->visible && !has_capability('moodle/course:viewhiddenactivities', $modulecontext)) {
- throw new moodle_exception('activityiscurrentlyhidden');
- }
-
- // Load the $post variable.
- $post = new stdClass();
- $post->course = $course->id;
- $post->moodleoverflow = $moodleoverflow->id;
- $post->discussion = $parent->discussion;
- $post->parent = $parent->id;
- $post->subject = $discussion->name;
- $post->userid = $USER->id;
- $post->message = '';
-
- // Append 'RE: ' to the discussions subject.
- $strre = get_string('re', 'moodleoverflow');
- if (!(substr($post->subject, 0, strlen($strre)) == $strre)) {
- $post->subject = $strre . ' ' . $post->subject;
- }
-
- // Unset where the user is coming from.
- // Allows to calculate the correct return url later.
- unset($SESSION->fromdiscussion);
-
-
-} else if (!empty($edit)) {
- // Third possibility: The user is editing his own post.
-
- // Check if the submitted post exists.
- if (!$post = moodleoverflow_get_post_full($edit)) {
- throw new moodle_exception('invalidpostid', 'moodleoverflow');
- }
-
- // Get the parent post of this post if it is not the starting post of the discussion.
- if ($post->parent) {
- if (!$parent = moodleoverflow_get_post_full($post->parent)) {
- throw new moodle_exception('invalidparentpostid', 'moodleoverflow');
- }
- }
-
- // Check if the post refers to a valid discussion.
- if (!$discussion = $DB->get_record('moodleoverflow_discussions', ['id' => $post->discussion])) {
- throw new moodle_exception('notpartofdiscussion', 'moodleoverflow');
- }
-
- // Check if the post refers to a valid moodleoverflow instance.
- if (!$moodleoverflow = $DB->get_record('moodleoverflow', ['id' => $discussion->moodleoverflow])) {
- throw new moodle_exception('invalidmoodleoverflowid', 'moodleoverflow');
- }
-
- // Check if the post refers to a valid course.
- if (!$course = $DB->get_record('course', ['id' => $discussion->course])) {
- throw new moodle_exception('invalidcourseid');
- }
-
- // Retrieve the related coursemodule.
- if (!$cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflow->id, $course->id)) {
- throw new moodle_exception('invalidcoursemodule');
- } else {
- $modulecontext = context_module::instance($cm->id);
- }
-
- // Set the pages context.
- $PAGE->set_cm($cm, $course, $moodleoverflow);
-
- // Check if the post can be edited.
- $beyondtime = ((time() - $post->created) > get_config('moodleoverflow', 'maxeditingtime'));
- $alreadyreviewed = review::should_post_be_reviewed($post, $moodleoverflow) && $post->reviewed;
- if (($beyondtime || $alreadyreviewed) && !has_capability('mod/moodleoverflow:editanypost', $modulecontext)) {
- throw new moodle_exception('maxtimehaspassed', 'moodleoverflow', '',
- format_time(get_config('moodleoverflow', 'maxeditingtime')));
- }
-
-
-
- // If the current user is not the one who posted this post.
- if ($post->userid <> $USER->id) {
-
- // Check if the current user has not the capability to edit any post.
- if (!has_capability('mod/moodleoverflow:editanypost', $modulecontext)) {
-
- // Display the error. Capabilities are missing.
- throw new moodle_exception('cannoteditposts', 'moodleoverflow');
- }
- }
-
- // Load the $post variable.
- $post->edit = $edit;
- $post->course = $course->id;
- $post->moodleoverflow = $moodleoverflow->id;
-
- // Unset where the user is coming from.
- // Allows to calculate the correct return url later.
- unset($SESSION->fromdiscussion);
-
-} else if (!empty($delete)) {
- // Fourth possibility: The user is deleting a post.
- // Check if the post is existing.
- if (!$post = moodleoverflow_get_post_full($delete)) {
- throw new moodle_exception('invalidpostid', 'moodleoverflow');
- }
-
- // Get the related discussion.
- if (!$discussion = $DB->get_record('moodleoverflow_discussions', ['id' => $post->discussion])) {
- throw new moodle_exception('notpartofdiscussion', 'moodleoverflow');
- }
-
- // Get the related moodleoverflow instance.
- if (!$moodleoverflow = $DB->get_record('moodleoverflow', ['id' => $discussion->moodleoverflow])) {
- throw new moodle_exception('invalidmoodleoverflowid', 'moodleoveflow');
- }
-
- // Get the related coursemodule.
- if (!$cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflow->id, $moodleoverflow->course)) {
- throw new moodle_exception('invalidcoursemodule');
- }
-
- // Get the related course.
- if (!$course = $DB->get_record('course', ['id' => $moodleoverflow->course])) {
- throw new moodle_exception('invalidcourseid');
- }
-
- // Require a login and retrieve the modulecontext.
- require_login($course, false, $cm);
- $modulecontext = context_module::instance($cm->id);
-
- // Check some capabilities.
- $deleteownpost = has_capability('mod/moodleoverflow:deleteownpost', $modulecontext);
- $deleteanypost = has_capability('mod/moodleoverflow:deleteanypost', $modulecontext);
- if (!(($post->userid == $USER->id && $deleteownpost) || $deleteanypost)) {
- throw new moodle_exception('cannotdeletepost', 'moodleoverflow');
- }
-
- // Count all replies of this post.
- $replycount = moodleoverflow_count_replies($post, false);
+// Now the post_control checks which interaction is wanted and builds a prepost.
+$postcontrol->detect_interaction($urlparameter);
+// If a post is being deleted, delete it immediately.
+if ($postcontrol->get_interaction() == 'delete') {
// Has the user confirmed the deletion?
if (!empty($confirm) && confirm_sesskey()) {
-
- // Check if the user has the capability to delete the post.
- $timepassed = time() - $post->created;
- if (($timepassed > get_config('moodleoverflow', 'maxeditingtime')) && !$deleteanypost) {
- $url = new moodle_url('/mod/moodleoverflow/discussion.php', ['d' => $post->discussion]);
- throw new moodle_exception('cannotdeletepost', 'moodleoverflow', moodleoverflow_go_back_to($url));
- }
-
- // A normal user cannot delete his post if there are direct replies.
- if ($replycount && !$deleteanypost) {
- $url = new moodle_url('/mod/moodleoverflow/discussion.php', ['d' => $post->discussion]);
- throw new moodle_exception('couldnotdeletereplies', 'moodleoverflow', moodleoverflow_go_back_to($url));
- } else {
- // Delete the post.
-
- // The post is the starting post of a discussion. Delete the topic as well.
- if (!$post->parent) {
- moodleoverflow_delete_discussion($discussion, $course, $cm, $moodleoverflow);
-
- // Trigger the discussion deleted event.
- $params = [
- 'objectid' => $discussion->id,
- 'context' => $modulecontext,
- ];
-
- $event = \mod_moodleoverflow\event\discussion_deleted::create($params);
- $event->trigger();
-
- // Redirect the user back to start page of the moodleoverflow instance.
- redirect("view.php?m=$discussion->moodleoverflow");
- exit;
-
- } else if (moodleoverflow_delete_post($post, $deleteanypost, $cm, $moodleoverflow)) {
- // Delete a single post.
- // Redirect back to the discussion.
- $discussionurl = new moodle_url('/mod/moodleoverflow/discussion.php', ['d' => $discussion->id]);
- redirect(moodleoverflow_go_back_to($discussionurl));
- exit;
-
- } else {
- // Something went wrong.
- throw new moodle_exception('errorwhiledelete', 'moodleoverflow');
- }
- }
+ $postcontrol->execute_delete();
} else {
// Deletion needs to be confirmed.
-
- moodleoverflow_set_return();
- $PAGE->navbar->add(get_string('delete', 'moodleoverflow'));
- $PAGE->set_title($course->shortname);
- $PAGE->set_heading($course->fullname);
-
- // The page should not be large, only pages containing broad tables are usually.
- $PAGE->add_body_class('limitedwidth');
-
- // Check if there are replies for the post.
- if ($replycount) {
-
- // Check if the user has capabilities to delete more than one post.
- if (!$deleteanypost) {
- throw new moodle_exception('couldnotdeletereplies', 'moodleoverflow',
- moodleoverflow_go_back_to(new moodle_url('/mod/moodleoverflow/discussion.php',
- ['d' => $post->discussion, 'p' . $post->id])));
- }
-
- // Request a confirmation to delete the post.
- echo $OUTPUT->header();
- echo $OUTPUT->confirm(get_string("deletesureplural", "moodleoverflow", $replycount + 1),
- "post.php?delete=$delete&confirm=$delete", $CFG->wwwroot . '/mod/moodleoverflow/discussion.php?d=' .
- $post->discussion . '#p' . $post->id);
-
+ $postcontrol->confirm_delete();
+
+ // Display a confirmation request depending on the number of posts that are being deleted.
+ $information = $postcontrol->get_information();
+ echo $OUTPUT->header();
+ if ($information->deletetype == 'plural') {
+ echo $OUTPUT->confirm(get_string('deletesureplural', 'moodleoverflow', $information->replycount + 1),
+ 'post.php?delete='.$delete.'&confirm='.$delete,
+ $CFG->wwwroot . '/mod/moodleoverflow/discussion.php?d=' . $information->discussion->get_id() .
+ '#p' . $information->relatedpost->get_id());
} else {
- // Delete a single post.
-
- // Print a confirmation message.
- echo $OUTPUT->header();
- echo $OUTPUT->confirm(get_string("deletesure", "moodleoverflow", $replycount),
+ echo $OUTPUT->confirm(get_string('deletesure', 'moodleoverflow', $information->replycount),
"post.php?delete=$delete&confirm=$delete",
- $CFG->wwwroot . '/mod/moodleoverflow/discussion.php?d=' . $post->discussion . '#p' . $post->id);
+ $CFG->wwwroot . '/mod/moodleoverflow/discussion.php?d=' . $information->discussion->get_id() .
+ '#p' . $information->relatedpost->get_id());
}
+ echo $OUTPUT->footer();
}
- echo $OUTPUT->footer();
exit;
-
-} else {
- // Last posibility: the action is not known.
-
- throw new moodle_exception('unknownaction');
}
-// Second step: The user must be logged on properly. Must be enrolled to the course as well.
-require_login($course, false, $cm);
-
-// Get the contexts.
-$modulecontext = context_module::instance($cm->id);
-$coursecontext = context_course::instance($course->id);
-
-// Get the subject.
-if ($edit) {
- $subject = $discussion->name;
-} else if ($reply) {
- $subject = $post->subject;
-} else if ($moodleoverflow) {
- $subject = $post->subject;
-}
+// A post will be created or edited. For that the post_control builds a post_form.
+$mformpost = $postcontrol->build_postform($pageparams);
-// Get attachments.
-$postid = empty($post->id) ? null : $post->id;
-$draftitemid = file_get_submitted_draft_itemid('attachments');
-file_prepare_draft_area($draftitemid,
- $modulecontext->id,
- 'mod_moodleoverflow',
- 'attachment',
- $postid,
- mod_moodleoverflow_post_form::attachment_options($moodleoverflow));
+// The User now entered information in the form. The post.php now needs to process the information and call the right function.
-if ($draftitemid && $edit && anonymous::is_post_anonymous($discussion, $moodleoverflow, $post->userid)
- && $post->userid != $USER->id) {
-
- $usercontext = context_user::instance($USER->id);
- $anonymousstr = get_string('anonymous', 'moodleoverflow');
- foreach (get_file_storage()->get_area_files($usercontext->id, 'user', 'draft', $draftitemid) as $file) {
- $file->set_author($anonymousstr);
- }
-}
+// Get attributes from the postcontrol.
+$information = $postcontrol->get_information();
+$prepost = $postcontrol->get_prepost();
-$draftideditor = file_get_submitted_draft_itemid('message');
-$currenttext = file_prepare_draft_area($draftideditor, $modulecontext->id, 'mod_moodleoverflow', 'post', $postid,
- mod_moodleoverflow_post_form::editor_options($modulecontext, $postid), $post->message);
-
-// Prepare the form.
-$formarray = [
- 'course' => $course,
- 'cm' => $cm,
- 'coursecontext' => $coursecontext,
- 'modulecontext' => $modulecontext,
- 'moodleoverflow' => $moodleoverflow,
- 'post' => $post,
- 'edit' => $edit,
-];
-$mformpost = new mod_moodleoverflow_post_form('post.php', $formarray, 'post', '', ['id' => 'mformmoodleoverflow']);
-
-// The current user is not the original author.
-// Append the message to the end of the message.
-if ($USER->id != $post->userid) {
-
- // Create a temporary object.
- $data = new stdClass();
- $data->date = userdate($post->modified);
- $post->messageformat = editors_get_preferred_format();
-
- // Append the message depending on the messages format.
- if ($post->messageformat == FORMAT_HTML) {
- $data->name = '' . fullname($USER) . '';
- $post->message .= '
';
-
- // Trigger the discussion created event.
- $params = [
- 'context' => $modulecontext,
- 'objectid' => $discussion->id,
- ];
- $event = \mod_moodleoverflow\event\discussion_created::create($params);
- $event->trigger();
- // Subscribe to this thread.
- $discussion->moodleoverflow = $moodleoverflow->id;
- \mod_moodleoverflow\subscriptions::moodleoverflow_post_subscription($moodleoverflow, $discussion, $modulecontext);
- }
-
- // Redirect back to te discussion.
- redirect(moodleoverflow_go_back_to($redirectto->out()), $message, null, \core\output\notification::NOTIFY_SUCCESS);
-
- // Do not continue.
- exit;
- }
+ $postcontrol->execute_interaction($fromform);
+ exit;
}
// If the script gets to this point, nothing has been submitted.
-// We have to display the form.
-// $course and $moodleoverflow are defined.
-// $discussion is only used for replying and editing.
+// The post_form will be displayed.
// Define the message to be displayed above the form.
$toppost = new stdClass();
-$toppost->subject = get_string("addanewdiscussion", "moodleoverflow");
+$toppost->subject = get_string('addanewdiscussion', 'moodleoverflow');
// Initiate the page.
-$PAGE->set_title("$course->shortname: $moodleoverflow->name " . format_string($toppost->subject));
-$PAGE->set_heading($course->fullname);
-
-// The page should not be large, only pages containing broad tables are usually.
+$PAGE->set_title($information->course->shortname . ': ' .
+ $information->moodleoverflow->name . ' ' .
+ format_string($toppost->subject));
+$PAGE->set_heading($information->course->fullname);
$PAGE->add_body_class('limitedwidth');
-// Display the header.
+// Display all.
echo $OUTPUT->header();
-
-// Display the form.
$mformpost->display();
-
-// Display the footer.
echo $OUTPUT->footer();
diff --git a/templates/discussion_list.mustache b/templates/discussion_list.mustache
index 1cb4a05c76..280097fe8d 100644
--- a/templates/discussion_list.mustache
+++ b/templates/discussion_list.mustache
@@ -35,16 +35,16 @@
{{! There are discussions. Start to print the table. }}
{{#hasdiscussions}}
-
diff --git a/templates/discussions.mustache b/templates/discussions.mustache
index 96d87a246a..11d78abe75 100644
--- a/templates/discussions.mustache
+++ b/templates/discussions.mustache
@@ -27,7 +27,7 @@
{{! There are no discussions. Print the string that specifies it. }}
{{^hasdiscussions}}
-
{{#currentdiscussion}}
diff --git a/templates/post.mustache b/templates/post.mustache
index e1002fcb54..3186b43c29 100644
--- a/templates/post.mustache
+++ b/templates/post.mustache
@@ -26,9 +26,9 @@
}}
{{! Print an anchor if the post is the first unread post of the discussion. }}
-{{# isfirstunread}}
+{{#isfirstunread}}
-{{/ isfirstunread}}
+{{/isfirstunread}}
{{! Start the post. Mark it read or unread. }}
diff --git a/tests/behat/behat_mod_moodleoverflow.php b/tests/behat/behat_mod_moodleoverflow.php
index 90675b688b..9965c0e557 100644
--- a/tests/behat/behat_mod_moodleoverflow.php
+++ b/tests/behat/behat_mod_moodleoverflow.php
@@ -244,4 +244,22 @@ public function should_not_exist_in_the_moodleoverflow_discussion_card($element,
$this->getSession()
);
}
+
+ /**
+ * Sets the limited answer starttime attribute of a moodleoverflow to the current time.
+ *
+ * @Given I set the :activity moodleoverflow limitedanswerstarttime to now
+ * @param string $activity
+ * @return void
+ */
+ public function i_set_the_moodleoverflow_limitedanswerstarttime_to_now($activity): void {
+ global $DB;
+
+ if (!$activityrecord = $DB->get_record('moodleoverflow', ['name' => $activity])) {
+ throw new Exception("Activity '$activity' not found");
+ }
+ // Update the specified field.
+ $activityrecord->la_starttime = time();
+ $DB->update_record('moodleoverflow', $activityrecord);
+ }
}
diff --git a/tests/behat/limitedanswer.feature b/tests/behat/limitedanswer.feature
new file mode 100644
index 0000000000..73e3b343c3
--- /dev/null
+++ b/tests/behat/limitedanswer.feature
@@ -0,0 +1,96 @@
+@mod @mod_moodleoverflow @javascript
+Feature: Moodleoverflows can start in a limited answer mode, where answers from students are not enabled until a set date.
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+
+ Scenario: With limited answer mode on, a teacher can answer a post that a student can not. When the teacher changes the
+ limitedanswer starttime to now, the student can now answer the post.
+ Given the following "activities" exist:
+ | activity | name | intro | course | idnumber | la_starttime |
+ | moodleoverflow | Test Moodleoverflow | Test moodleoverflow description | C1 | 1 | ##now +1 day## |
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "Test Moodleoverflow"
+ And I add a new discussion to "Test Moodleoverflow" moodleoverflow with:
+ | Subject | Forum post 1 |
+ | Message | This is the question message |
+ And I log out
+ And I log in as "student1"
+ And I am on "Course 1" course homepage
+ And I follow "Test Moodleoverflow"
+ And I follow "Forum post 1"
+ And I click on "Answer" "text"
+ Then I should not see "Your reply"
+ When I set the "Test Moodleoverflow" moodleoverflow limitedanswerstarttime to now
+ And I am on "Course 1" course homepage
+ And I follow "Test Moodleoverflow"
+ And I follow "Forum post 1"
+ And I click on "Answer" "text"
+ Then I should see "Your reply"
+ And I set the following fields to these values:
+ | Subject | Re: Forum post 1 |
+ | Message | This is the answer message |
+ And I press "Post to forum"
+ Then I should see "This is the answer message"
+ And I should see "This is the question message"
+
+ Scenario: Setting up the limited answer mode, the times need to be in the right order
+ Given the following "activities" exist:
+ | activity | name | intro | course | idnumber |
+ | moodleoverflow | Test Moodleoverflow | Test moodleoverflow description | C1 | 1 |
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "Test Moodleoverflow"
+ And I navigate to "Settings" in current page administration
+ And I follow "Limited Answer Mode"
+ And I click on "la_starttime[enabled]" "checkbox"
+ And I set the following fields to these values:
+ | id_la_starttime_day | ##tomorrow##%d## |
+ | id_la_starttime_month | ##tomorrow##%B## |
+ | id_la_starttime_year | ##tomorrow##%Y## |
+ | id_la_starttime_hour | 12 |
+ | id_la_starttime_minute | 30 |
+ And I click on "la_endtime[enabled]" "checkbox"
+ And I set the following fields to these values:
+ | id_la_endtime_day | ##yesterday##%d## |
+ | id_la_endtime_month | ##yesterday##%B## |
+ | id_la_endtime_year | ##yesterday##%Y## |
+ | id_la_endtime_hour | 12 |
+ | id_la_endtime_minute | 30 |
+ When I press "Save and display"
+ And I follow "Limited Answer Mode"
+ And I click on "#collapseElement-5" "css_element"
+ Then I should see "End time must be in the future"
+ And I should see "The end time must be after the start time"
+
+ Scenario: Setting up the limited answer mode, the start times need to be in the future
+ Given the following "activities" exist:
+ | activity | name | intro | course | idnumber |
+ | moodleoverflow | Test Moodleoverflow | Test moodleoverflow description | C1 | 1 |
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "Test Moodleoverflow"
+ And I navigate to "Settings" in current page administration
+ And I follow "Limited Answer Mode"
+ And I click on "la_starttime[enabled]" "checkbox"
+ And I set the following fields to these values:
+ | id_la_starttime_day | ##yesterday##%d## |
+ | id_la_starttime_month | ##yesterday##%B## |
+ | id_la_starttime_year | ##yesterday##%Y## |
+ | id_la_starttime_hour | 12 |
+ | id_la_starttime_minute | 30 |
+ When I press "Save and display"
+ And I follow "Limited Answer Mode"
+ And I click on "#collapseElement-5" "css_element"
+ Then I should see "Start time must be in the future"
diff --git a/tests/discussion_test.php b/tests/discussion_test.php
new file mode 100644
index 0000000000..e5ee1c6c31
--- /dev/null
+++ b/tests/discussion_test.php
@@ -0,0 +1,164 @@
+.
+
+/**
+ * PHP Unit Tests for the Discussion class.
+ * @package mod_moodleoverflow
+ * @copyright 2023 Tamaro Walter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_moodleoverflow;
+
+use mod_moodleoverflow\post\post;
+use mod_moodleoverflow\discussion\discussion;
+
+/**
+ * Tests if the functions from the discussion class are working correctly.
+ * As the discussion class works as an administrator of the post class, most of the testcases are already realized in the
+ * post_test.php file.
+ * @package mod_moodleoverflow
+ * @copyright 2023 Tamaro Walter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @covers \mod_moodleoverflow\discussion\discussion
+ */
+final class discussion_test extends \advanced_testcase {
+
+ /** @var \stdClass test course */
+ private $course;
+
+ /** @var \stdClass coursemodule */
+ private $coursemodule;
+
+ /** @var \stdClass modulecontext */
+ private $modulecontext;
+
+ /** @var \stdClass test moodleoverflow */
+ private $moodleoverflow;
+
+ /** @var \stdClass test teacher */
+ private $teacher;
+
+ /** @var discussion a discussion */
+ private $discussion;
+
+ /** @var post the post from the discussion */
+ private $post;
+
+ /** @var \mod_moodleoverflow_generator $generator */
+ private $generator;
+
+ public function setUp(): void {
+ parent::setUp();
+ $this->resetAfterTest();
+ $this->helper_course_set_up();
+ }
+
+ public function tearDown(): void {
+ // Clear all caches.
+ subscriptions::reset_moodleoverflow_cache();
+ subscriptions::reset_discussion_cache();
+ parent::tearDown();
+ }
+
+ /**
+ * Test, if a discussion is being created correctly
+ */
+ public function test_create_discussion(): void {
+ global $DB;
+
+ // Build a prepost object with important information.
+ $time = time();
+ $prepost = new \stdClass();
+ $prepost->userid = $this->teacher->id;
+ $prepost->timenow = $time;
+ $prepost->message = 'a message';
+ $prepost->messageformat = 1;
+ $prepost->reviewed = 0;
+ $prepost->formattachments = '';
+ $prepost->modulecontext = $this->modulecontext;
+
+ // Build a new discussion object.
+ $discussion = discussion::construct_without_id($this->course->id, $this->moodleoverflow->id, 'Discussion Topic',
+ 0, $this->teacher->id, $time, $time, $this->teacher->id);
+ $discussionid = $discussion->moodleoverflow_add_discussion($prepost);
+ $posts = $discussion->moodleoverflow_get_discussion_posts();
+ $post = $posts[$discussion->get_firstpostid()];
+
+ // The discussion and the firstpost should be in the DB.
+ $dbdiscussion = $DB->get_record('moodleoverflow_discussions', ['id' => $discussion->get_id()]);
+ $this->assertEquals($dbdiscussion->id, $discussionid);
+ $this->assertEquals('Discussion Topic', $dbdiscussion->name);
+
+ $dbpost = $DB->get_record('moodleoverflow_posts', ['id' => $discussion->get_firstpostid()]);
+ $this->assertEquals($dbpost->id, $post->get_id());
+ $this->assertEquals($dbpost->discussion, $post->get_discussionid());
+ $this->assertEquals($prepost->message, $dbpost->message);
+ }
+
+ /**
+ * Test, if a post and its attachment are deleted successfully.
+ * @covers ::moodleoverflow_delete_post
+ */
+ public function test_delete_discussion(): void {
+ global $DB;
+ // Build the prepost object with necessary information.
+ $prepost = new \stdClass();
+ $prepost->modulecontext = $this->modulecontext;
+
+ // Delete the discussion, but save the IDs first.
+ $discussionid = $this->discussion->get_id();
+ $postid = $this->discussion->get_firstpostid();
+ $this->discussion->moodleoverflow_delete_discussion($prepost);
+
+ // The discussion and the post should not be in the DB anymore.
+ $discussion = count($DB->get_records('moodleoverflow_discussions', ['id' => $discussionid]));
+ $this->assertEquals(0, $discussion);
+
+ $post = count($DB->get_records('moodleoverflow_posts', ['id' => $postid]));
+ $this->assertEquals(0, $post);
+ }
+
+ /**
+ * This function creates:
+ * - a course with a moodleoverflow
+ * - a new discussion with a post. The post has an attachment.
+ */
+ private function helper_course_set_up() {
+ global $DB;
+ // Create a new course with a moodleoverflow forum.
+ $this->course = $this->getDataGenerator()->create_course();
+ $location = ['course' => $this->course->id];
+ $this->moodleoverflow = $this->getDataGenerator()->create_module('moodleoverflow', $location);
+ $this->coursemodule = get_coursemodule_from_instance('moodleoverflow', $this->moodleoverflow->id);
+ $this->modulecontext = \context_module::instance($this->coursemodule->id);
+
+ // Create a teacher.
+ $this->teacher = $this->getDataGenerator()->create_user(['firstname' => 'Tamaro', 'lastname' => 'Walter']);
+ $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, 'student');
+
+ // Create a discussion started from the teacher.
+ $this->generator = $this->getDataGenerator()->get_plugin_generator('mod_moodleoverflow');
+ $discussion = $this->generator->post_to_forum($this->moodleoverflow, $this->teacher);
+
+ // Get the discussion and post object.
+ $discussionrecord = $DB->get_record('moodleoverflow_discussions', ['id' => $discussion[0]->id]);
+ $postrecord = $DB->get_record('moodleoverflow_posts', ['id' => $discussion[1]->id]);
+
+ $this->discussion = discussion::from_record($discussionrecord);
+ $this->post = post::from_record($postrecord);
+ }
+}
diff --git a/tests/post_test.php b/tests/post_test.php
index a4727b0b3f..8b16739bbd 100644
--- a/tests/post_test.php
+++ b/tests/post_test.php
@@ -15,7 +15,7 @@
// along with Moodle. If not, see .
/**
- * PHP Unit test for post related functions in the locallib.
+ * PHP Unit Tests for the Post class.
*
* @package mod_moodleoverflow
* @copyright 2023 Tamaro Walter
@@ -23,43 +23,50 @@
*/
namespace mod_moodleoverflow;
+// Use the post class.
+use context;
+use mod_moodleoverflow\post\post;
+use mod_moodleoverflow\discussion\discussion;
+use stdClass;
+
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../locallib.php');
/**
- * PHP Unit test for post related functions in the locallib.
+ *
+ * Tests if the functions from the post class are working correctly.
*
* @package mod_moodleoverflow
* @copyright 2023 Tamaro Walter
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @covers \mod_moodleoverflow\post\post
*/
final class post_test extends \advanced_testcase {
- /** @var \stdClass test course */
+ /** @var stdClass test course */
private $course;
- /** @var \stdClass coursemodule */
+ /** @var stdClass coursemodule */
private $coursemodule;
- /** @var \stdClass test moodleoverflow */
+ /** @var stdClass modulecontext */
+ private $modulecontext;
+
+ /** @var stdClass test moodleoverflow */
private $moodleoverflow;
- /** @var \stdClass test teacher */
+ /** @var stdClass test teacher */
private $teacher;
- /** @var \stdClass a discussion */
+ /** @var discussion a discussion */
private $discussion;
- /** @var \stdClass a post */
+ /** @var post a post */
private $post;
- /** @var \stdClass an attachment */
- private $attachment;
-
/** @var \mod_moodleoverflow_generator $generator */
private $generator;
-
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
@@ -68,47 +75,79 @@ public function setUp(): void {
public function tearDown(): void {
// Clear all caches.
- \mod_moodleoverflow\subscriptions::reset_moodleoverflow_cache();
- \mod_moodleoverflow\subscriptions::reset_discussion_cache();
+ subscriptions::reset_moodleoverflow_cache();
+ subscriptions::reset_discussion_cache();
parent::tearDown();
}
/**
- * Test if a post and its attachment are deleted successfully.
- * @covers ::moodleoverflow_delete_post
+ * Test, if a post is being created correctly
*/
- public function test_moodleoverflow_delete_post(): void {
+ public function test_create_post(): void {
+ global $DB;
+ // Build a new post object.
+ $time = time();
+ $message = 'a unique message';
+ $post = post::construct_without_id($this->discussion->get_id(), $this->post->get_id(), $this->teacher->id, $time,
+ $time, $message, 0, '', 0, 1, null);
+ $post->moodleoverflow_add_new_post();
+
+ // The post should be in the database.
+ $postscount = count($DB->get_records('moodleoverflow_posts', ['id' => $post->get_id()]));
+ $this->assertEquals(1, $postscount);
+ }
+
+ /**
+ * Test, if the message of a post can be edited successfully.
+ */
+ public function test_edit_post(): void {
global $DB;
- // The attachment should exist.
- $numberofattachments = count($DB->get_records('files', ['itemid' => $this->post->id]));
- $this->assertEquals(2, $numberofattachments);
+ // The post and the attachment should exist.
+ $numberofattachments = count($DB->get_records('files', ['itemid' => $this->post->get_id()]));
+ $this->assertEquals(2, $numberofattachments); // One Attachment is saved twice in 'files'.
+ $post = count($DB->get_records('moodleoverflow_posts', ['id' => $this->post->get_id()]));
+ $this->assertEquals(1, $post);
- // Delete the post from the teacher with its attachment.
- moodleoverflow_delete_post($this->post, false, $this->coursemodule, $this->moodleoverflow);
+ // Gather important parameters.
+ $message = 'a new message';
- // Now try to get the attachment.
- $numberofattachments = count($DB->get_records('files', ['itemid' => $this->post->id]));
+ $time = time();
- $this->assertEquals(0, $numberofattachments);
+ // Update the post.
+ $this->post->moodleoverflow_edit_post($time, $message, $this->post->messageformat, $this->post->formattachments);
+
+ // The message and modified time should be changed.
+ $post = $DB->get_record('moodleoverflow_posts', ['id' => $this->post->get_id()]);
+ $this->assertEquals($message, $post->message);
+ $this->assertEquals($time, $post->modified);
}
/**
- * Test if a post and its attachment are deleted successfully.
- * @covers ::moodleoverflow_delete_discussion
+ * Test, if a post and its attachment are deleted successfully.
+ * @covers ::moodleoverflow_delete_post
*/
- public function test_moodleoverflow_delete_discussion(): void {
+ public function test_moodleoverflow_delete_post(): void {
global $DB;
- $numberofattachments = count($DB->get_records('files', ['itemid' => $this->post->id, 'filearea' => 'attachment']));
- $this->assertEquals(2, $numberofattachments);
+ // The post and the attachment should exist.
+ $numberofattachments = count($DB->get_records('files', ['itemid' => $this->post->get_id()]));
+ $this->assertEquals(2, $numberofattachments); // One Attachment is saved twice in 'files'.
+ $post = count($DB->get_records('moodleoverflow_posts', ['id' => $this->post->get_id()]));
+ $this->assertEquals(1, $post);
- // Delete the post from the teacher with its attachment.
- moodleoverflow_delete_discussion($this->discussion[0], $this->course, $this->coursemodule, $this->moodleoverflow);
+ // Delete the post with its attachment.
+ // Save the post id as it gets unsettled by the post object after being deleted.
+ $postid = $this->post->get_id();
+ $this->post->moodleoverflow_delete_post(true);
- // Now try to get the attachment.
- $numberofattachments = count($DB->get_records('files', ['itemid' => $this->post->id]));
+ // Now try to get the attachment, it should be deleted from the database.
+ $numberofattachments = count($DB->get_records('files', ['itemid' => $postid]));
$this->assertEquals(0, $numberofattachments);
+
+ // Try to find the post, it should be deleted.
+ $post = count($DB->get_records('moodleoverflow_posts', ['id' => $postid]));
+ $this->assertEquals(0, $post);
}
/**
@@ -123,6 +162,7 @@ private function helper_course_set_up() {
$location = ['course' => $this->course->id];
$this->moodleoverflow = $this->getDataGenerator()->create_module('moodleoverflow', $location);
$this->coursemodule = get_coursemodule_from_instance('moodleoverflow', $this->moodleoverflow->id);
+ $this->modulecontext = \context_module::instance($this->coursemodule->id);
// Create a teacher.
$this->teacher = $this->getDataGenerator()->create_user(['firstname' => 'Tamaro', 'lastname' => 'Walter']);
@@ -130,29 +170,41 @@ private function helper_course_set_up() {
// Create a discussion started from the teacher.
$this->generator = $this->getDataGenerator()->get_plugin_generator('mod_moodleoverflow');
- $this->discussion = $this->generator->post_to_forum($this->moodleoverflow, $this->teacher);
- $this->post = $DB->get_record('moodleoverflow_posts', ['id' => $this->discussion[0]->firstpost], '*');
+ $discussion = $this->generator->post_to_forum($this->moodleoverflow, $this->teacher);
+ $discussionrecord = $DB->get_record('moodleoverflow_discussions', ['id' => $discussion[0]->id]);
+ $this->discussion = discussion::from_record($discussionrecord);
+
+ // Get a temporary post from the DB to add the attachment.
+ $temppost = $DB->get_record('moodleoverflow_posts', ['id' => $this->discussion->get_firstpostid()]);
// Create an attachment by inserting it directly in the database and update the post record.
+ $this->add_new_attachment($temppost, $this->modulecontext, 'world.txt', 'hello world');
- $modulecontext = \context_module::instance($this->coursemodule->id);
+ // Build the real post object now. That is the object that will be tested.
+ $postrecord = $DB->get_record('moodleoverflow_posts', ['id' => $this->discussion->get_firstpostid()]);
+ $this->post = post::from_record($postrecord);
+ }
+ /**
+ * Adds a new attachment to a post.
+ *
+ * @param stdClass $object The post object to which the attachment should be added.
+ * @param context $modulecontext The context of the module.
+ * @param string $filename The name of the file to be added.
+ * @param string $filecontent The content of the file to be added.
+ */
+ private function add_new_attachment($object, $modulecontext, $filename, $filecontent) {
+ global $DB;
$fileinfo = [
- 'contextid' => $modulecontext->id, // ID of the context.
- 'component' => 'mod_moodleoverflow', // Your component name.
- 'filearea' => 'attachment', // Usually = table name.
- 'itemid' => $this->post->id, // Usually = ID of row in table.
- 'filepath' => '/', // Any path beginning and ending in /.
- 'filename' => 'NH.jpg', // Any filename.
+ 'contextid' => $modulecontext->id, // ID of the context.
+ 'component' => 'mod_moodleoverflow', // Your component name.
+ 'filearea' => 'attachment', // Usually = table name.
+ 'itemid' => $object->id, // Usually = ID of the item (e.g. the post.
+ 'filepath' => '/', // Any path beginning and ending in /.
+ 'filename' => $filename, // Any filename.
];
-
$fs = get_file_storage();
-
- // Create a new file containing the text 'hello world'.
- $fs->create_file_from_string($fileinfo, 'hello world');
-
- $this->post->attachment = 1;
- $DB->update_record('moodleoverflow_posts', $this->post);
-
+ $fs->create_file_from_string($fileinfo, $filecontent); // Creates a new file containing the text 'hello world'.
+ $DB->update_record('moodleoverflow_posts', $object);
}
}
diff --git a/tests/ratings_test.php b/tests/ratings_test.php
index a1f8cf30ec..5c3682ce5e 100644
--- a/tests/ratings_test.php
+++ b/tests/ratings_test.php
@@ -39,6 +39,26 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class ratings_test extends \advanced_testcase {
+ /** @var stdClass test course */
+ private $course;
+
+ /** @var stdClass coursemodule */
+ private $coursemodule;
+
+ /** @var stdClass test moodleoverflow */
+ private $moodleoverflow;
+
+ /** @var stdClass test teacher */
+ private $teacher;
+
+ /** @var stdClass test user */
+ private $user1;
+
+ /** @var stdClass another test user */
+ private $user2;
+
+ /** @var stdClass a discussion */
+ private $discussion;
/** @var stdClass a post from the teacher*/
private $post;
@@ -61,6 +81,9 @@ final class ratings_test extends \advanced_testcase {
/** @var stdClass answer from user 2 */
private $answer6;
+ /** @var \mod_moodleoverflow_generator $generator */
+ private $generator;
+
/**
* Test setUp.
*/
diff --git a/tests/subscriptions_test.php b/tests/subscriptions_test.php
index 9e5a76e47e..e5a695249c 100644
--- a/tests/subscriptions_test.php
+++ b/tests/subscriptions_test.php
@@ -1228,7 +1228,7 @@ public function test_is_subscribable_is_guest($options): void {
}
/**
- * Returns subscription obtions.
+ * Returns subscription options.
* @return array
*/
public static function is_subscribable_loggedin_provider(): array {
diff --git a/tests/userstats_test.php b/tests/userstats_test.php
index 61a670129c..45a82819c9 100644
--- a/tests/userstats_test.php
+++ b/tests/userstats_test.php
@@ -94,8 +94,8 @@ public function setUp(): void {
*/
public function tearDown(): void {
// Clear all caches.
- \mod_moodleoverflow\subscriptions::reset_moodleoverflow_cache();
- \mod_moodleoverflow\subscriptions::reset_discussion_cache();
+ subscriptions::reset_moodleoverflow_cache();
+ subscriptions::reset_discussion_cache();
parent::tearDown();
}
@@ -223,7 +223,7 @@ public function test_partial_anonymous(): void {
}
/**
- * Test, if userstats are calculated correctly if the moodleoverflow is partially anonymous.
+ * Test, if userstats are calculated correctly if the moodleoverflow is totally anonymous.
* @covers \userstats_table
*/
public function test_total_anonymous(): void {
@@ -232,6 +232,7 @@ public function test_total_anonymous(): void {
// Get the current userstats to compare later.
$olduserstats = $this->create_statstable();
+ $oldupvotesuser1 = $this->get_specific_userstats($olduserstats, $this->user1, 'receivedupvotes');
$oldactivityuser1 = $this->get_specific_userstats($olduserstats, $this->user1, 'forumactivity');
$oldupvotesuser2 = $this->get_specific_userstats($olduserstats, $this->user2, 'receivedupvotes');
@@ -304,7 +305,7 @@ private function helper_course_set_up() {
* Makes the existing moodleoverflow anonymous.
* There are 2 types of anonymous moodleoverflows:
* anonymous = 1, the topic starter is anonymous
- * anonymous = 2, all users are anonym
+ * anonymous = 2, all users are anonymous
*
* @param int $anonymoussetting
*/
diff --git a/version.php b/version.php
index 2fb13655ac..6f099b71c6 100644
--- a/version.php
+++ b/version.php
@@ -17,19 +17,19 @@
/**
* Defines the version and other meta-info about the plugin
*
- * Setting the $plugin->version to 0 prevents the plugin from being installed.
* See https://docs.moodle.org/dev/version.php for more info.
*
* @package mod_moodleoverflow
- * @copyright 2017 Kennet Winter
+ * @copyright 2025 Thomas Niedermaier, University MΓΌnster
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
+$plugin->version = 2025070700;
+$plugin->requires = 2022112819; // Require Moodle 4.1.
+$plugin->supported = [401, 500];
$plugin->component = 'mod_moodleoverflow';
-$plugin->version = 2025030500;
-$plugin->release = 'v4.4-r3';
-$plugin->requires = 2022112800; // Requires 4.1+ Moodle version.
-$plugin->maturity = MATURITY_STABLE;
+$plugin->maturity = MATURITY_RC;
+$plugin->release = 'v5.0-rc1';
$plugin->dependencies = [];