diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index f1fb3f254f..c020ab22d4 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -8,8 +8,8 @@ jobs: strategy: matrix: - php: ['8.0'] - moodle-branch: ['MOODLE_401_STABLE'] + php: ['8.1'] + moodle-branch: ['MOODLE_402_STABLE'] database: ['pgsql'] steps: @@ -110,8 +110,8 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.0'] - moodle-branch: ['MOODLE_311_STABLE', 'MOODLE_400_STABLE', 'MOODLE_401_STABLE'] + php: ['8.0', '8.1'] + moodle-branch: ['MOODLE_401_STABLE', 'MOODLE_402_STABLE'] database: ['mariadb', 'pgsql'] include: - php: '7.4' @@ -120,6 +120,18 @@ jobs: - php: '7.4' moodle-branch: 'MOODLE_39_STABLE' database: 'pgsql' + - php: '8.0' + moodle-branch: 'MOODLE_311_STABLE' + database: 'mariadb' + - php: '8.0' + moodle-branch: 'MOODLE_311_STABLE' + database: 'pgsql' + - php: '8.0' + moodle-branch: 'MOODLE_400_STABLE' + database: 'mariadb' + - php: '8.0' + moodle-branch: 'MOODLE_400_STABLE' + database: 'pgsql' steps: - name: Start MariaDB diff --git a/.github/workflows/moodle-release.yml b/.github/workflows/moodle-release.yml index 97c70a6554..317a7916fa 100644 --- a/.github/workflows/moodle-release.yml +++ b/.github/workflows/moodle-release.yml @@ -44,7 +44,7 @@ jobs: --data-urlencode "altdownloadurl=${ZIPURL}" \ --data-urlencode "releasenotes=${BODY}" \ --data-urlencode "releasenotesformat=4") - echo "::set-output name=response::${RESPONSE}" + echo "response=${RESPONSE}" >> $GITHUB_OUTPUT - name: Evaluate the response id: evaluate-response env: diff --git a/amd/build/rating.min.js.map b/amd/build/rating.min.js.map index 990ff940d3..8243d74e75 100644 --- a/amd/build/rating.min.js.map +++ b/amd/build/rating.min.js.map @@ -1 +1,2 @@ -{"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 */\nexport function init(userid) {\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 ? 'statusstarter' : 'statusteacher';\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 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 if (!shouldRemove) {\n postElement.classList.add(htmlclass);\n actionElement.textContent = await getString(`marknot${action}`, 'mod_moodleoverflow');\n }\n }\n }\n };\n\n}"],"names":["userid","prefetchStrings","root","onclick","event","actionElement","target","closest","action","getAttribute","postElement","postid","isupvote","sendVote","RATING_REMOVE_UPVOTE","RATING_REMOVE_DOWNVOTE","setAttribute","title","otherAction","RATING_UPVOTE","RATING_DOWNVOTE","otherElement","querySelector","htmlclass","isHelpful","shouldRemove","classList","contains","baseRating","RATING_HELPFUL","RATING_SOLVED","rating","querySelectorAll","el","remove","textContent","add","document","getElementById","Ajax","call","methodname","args","ratingid","response","forEach","i","raterreputation","ownerid","ownerreputation","postrating"],"mappings":"8kEAoEqBA,0BACRC,gBAAgB,qBACrB,CAAC,aAAc,gBAAiB,cAAe,iBAC3C,uBAAwB,gBAAiB,yBAA0B,oBAE3EC,KAAKC,yDAAU,iBAAMC,mQACXC,cAAgBD,MAAME,OAAOC,QAAQ,+FAKrCC,OAASH,cAAcI,aAAa,8BACpCC,YAAcL,cAAcE,QAAQ,gCACpCI,OAASD,yBAAAA,YAAaD,aAAa,0CAEjCD,qBACC,wBACA,2BAkBA,yBACA,6CAlBKI,SAAsB,WAAXJ,OAC+C,YAA5DH,cAAcI,aAAa,6EACrBI,SAASF,OAAQC,SAAWE,qBAAuBC,uBAAwBf,uBACjFK,cAAcW,aAAa,4BAA6B,gCAC5B,mBAAU,UAAYR,OAAQ,8BAA1DH,cAAcY,0DAERC,YAAcN,SAAW,WAAa,0BACtCC,SAASF,OAAQC,SAAWO,cAAgBC,gBAAiBpB,uBACnEK,cAAcW,aAAa,4BAA6B,YAClDK,aAAeX,YAAYY,qDACGJ,oBACvBF,aAAa,4BAA6B,gCAC3B,mBAAU,iBAAmBR,OAAQ,qCAAjEH,cAAcY,sCACa,mBAAU,UAAYC,YAAa,8BAA9DG,aAAaJ,8EAOXM,WADAC,UAAuB,YAAXhB,QACY,gBAAkB,gBAC1CiB,aAAef,YAAYgB,UAAUC,SAASJ,WAC9CK,WAAaJ,UAAYK,eAAiBC,cAC1CC,OAASN,aAA4B,GAAbG,WAAkBA,4BAC1Cf,SAASF,OAAQoB,OAAQ/B,qDACdE,KAAK8B,iBAAiB,uBAAyBT,gHAArDU,gBACJP,UAAUQ,OAAOX,6BAEV,iCAAiBf,QAAU,8BADrCyB,GAAGX,qDAA8Cd,cAAY2B,sPAG5DV,4CACDf,YAAYgB,UAAUU,IAAIb,6BACQ,oCAAoBf,QAAU,8BAAhEH,cAAc8B,wPA5F5Bf,gBAAkB,EAClBD,cAAgB,EAChBJ,uBAAyB,GACzBD,qBAAuB,GACvBgB,cAAgB,EAChBD,eAAiB,EAEjB3B,KAAOmC,SAASC,eAAe,gCAStBzB,6IAAf,kBAAwBF,OAAQoB,OAAQ/B,qJACbuC,cAAKC,KAAK,CAAC,CAC9BC,WAAY,iCACZC,KAAM,CACF/B,OAAQA,OACRgC,SAAUZ,WAEd,iBANEa,wBAON1C,KAAK8B,gEAAyDhC,cAAY6C,SAAQ,SAACC,GAC/EA,EAAEX,YAAcS,SAASG,mBAE7B7C,KAAK8B,gEAAyDY,SAASI,eAAaH,SAAQ,SAACC,GACzFA,EAAEX,YAAcS,SAASK,mBAE7B/C,KAAK8B,gEAAyDrB,cAAYkC,SAAQ,SAACC,GAC/EA,EAAEX,YAAcS,SAASM,wCAEtBN"} \ 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 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 ? 'statusstarter' : 'statusteacher';\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 then 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 usable 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","RATING_REMOVE_UPVOTE","RATING_REMOVE_DOWNVOTE","setAttribute","title","otherAction","RATING_UPVOTE","RATING_DOWNVOTE","otherElement","querySelector","isHelpful","htmlclass","shouldRemove","classList","contains","baseRating","RATING_HELPFUL","RATING_SOLVED","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,oBACLC,cAAgBC,MAAMC,OAAOC,QAAQ,oCACtCH,2BAICI,OAASJ,cAAcK,aAAa,8BACpCC,YAAcN,cAAcG,QAAQ,gCACpCI,OAASD,yBAAAA,YAAaD,aAAa,qCAEjCD,YACC,aACA,kBACKI,SAAsB,WAAXJ,UAC+C,YAA5DJ,cAAcK,aAAa,mCACrBI,SAASF,OAAQC,SAAWE,qBAAuBC,uBAAwBjB,QACjFM,cAAcY,aAAa,4BAA6B,cACxDZ,cAAca,YAAc,mBAAU,UAAYT,OAAQ,0BACvD,OACGU,YAAcN,SAAW,WAAa,eACtCC,SAASF,OAAQC,SAAWO,cAAgBC,gBAAiBtB,QACnEM,cAAcY,aAAa,4BAA6B,iBAClDK,aAAeX,YAAYY,qDACGJ,mBACpCG,aAAaL,aAAa,4BAA6B,cACvDZ,cAAca,YAAc,mBAAU,iBAAmBT,OAAQ,sBACjEa,aAAaJ,YAAc,mBAAU,UAAYC,YAAa,iCAIjE,cACA,gBACKK,UAAuB,YAAXf,OACZgB,UAAYD,UAAY,gBAAkB,gBAC1CE,aAAef,YAAYgB,UAAUC,SAASH,WAC9CI,WAAaL,UAAYM,eAAiBC,cAC1CC,OAASN,aAA4B,GAAbG,WAAkBA,oBAC1Cf,SAASF,OAAQoB,OAAQjC,QAO1BC,mBASG0B,eACAf,YAAYgB,UAAUM,OAAOR,WAC7BpB,cAAc6B,kBAAoB,iCAAiBzB,QAAU,sBAC7D0B,cAAcV,UAAWhB,kBAVxB,MAAM2B,MAAMlC,KAAKmC,iBAAiB,uBAAyBZ,WAC5DW,GAAGT,UAAUM,OAAOR,WACpBW,GAAGb,qDAA8Cd,cAAYyB,kBACnD,iCAAiBzB,QAAU,sBAWxCiB,eACDf,YAAYgB,UAAUW,IAAIb,WAC1BpB,cAAc6B,kBAAoB,oCAAoBzB,QAAU,sBAC5DT,oBACAmC,cAAcV,UAAWhB,mGAjH3CY,gBAAkB,EAClBD,cAAgB,EAChBJ,uBAAyB,GACzBD,qBAAuB,GACvBgB,cAAgB,EAChBD,eAAiB,EAEjB5B,KAAOqC,SAASC,eAAe,sCAStB1B,SAASF,OAAQoB,OAAQjC,cAC9B0C,eAAiBC,cAAKC,KAAK,CAAC,CAC9BC,WAAY,iCACZC,KAAM,CACFjC,OAAQA,OACRkC,SAAUd,WAEd,UACJ9B,KAAKmC,gEAAyDtC,cAAYgD,SAASC,IAC/EA,EAAEd,YAAcO,SAASQ,eAAzB,IAEJ/C,KAAKmC,gEAAyDI,SAASS,eAAaH,SAASC,IACzFA,EAAEd,YAAcO,SAASU,eAAzB,IAEJjD,KAAKmC,gEAAyDzB,cAAYmC,SAASC,IAC/EA,EAAEd,YAAcO,SAASW,UAAzB,IAEGX,wBAgGIN,cAAcV,UAAWhB,0BAC3BR,gBAAgB,qBACrB,CAAC,aAAc,iBAAkB,cAAe,wBAGhDoD,kBAAmB,MAClB,MAAMjB,MAAMlC,KAAKmC,iBAAiB,0BAC/BD,GAAGT,UAAUC,SAASH,WAAY,CAClC4B,kBAAmB,YAKtB,MAAMjB,MAAMlC,KAAKmC,iBAAiB,wBAC9BD,GAAGT,UAAUC,SAASH,YAAcW,GAAGb,qDAA8Cd,gBAElF2B,GAAGb,qDAA8Cd,cAAYyB,YAD7DmB,uBAEU,qCAAqB5C,QAAU,4BAG/B,iCAAiBA,QAAU"} + diff --git a/amd/build/reviewing.min.js.map b/amd/build/reviewing.min.js.map index d2b01e0044..0f8f0c1d6b 100644 --- a/amd/build/reviewing.min.js.map +++ b/amd/build/reviewing.min.js.map @@ -1 +1,2 @@ -{"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","e","action","target","getAttribute","post","closest","reviewRow","postID","innerHTML","Ajax","call","methodname","args","postid","nextPostURL","message","classList","remove","Templates","render","rejectMessage","querySelector","value","toString","trim","reason"],"mappings":"8iBA+BaA,kBAAkB,CAAC,sCAAuC,wDAC1DC,gBAAgB,qBACrB,CAAC,oBAAqB,mCAAoC,oCAAqC,sBAEtFC,SAASC,eAAe,wBAChCC,qCAAU,iBAAMC,yMACXC,OAASD,EAAEE,OAAOC,aAAa,gGAM/BC,KAAOJ,EAAEE,OAAOG,QAAQ,iCACxBC,UAAYN,EAAEE,OAAOG,QAAQ,cAC7BE,OAASH,KAAKD,aAAa,8BAElB,YAAXF,sCACAK,UAAUE,UAAY,qBACIC,cAAKC,KAAK,CAAC,CACjCC,WAAY,yCACZC,KAAM,CACFC,OAAQN,WAEZ,kBALEO,4CAOc,mBAAU,oBAAqB,2DAA/CC,oBAAuE,KACvED,uDACAC,uCAAuBD,oCACX,mBAAU,mCAAoC,4FAD1DC,iCAEM,yDAENA,0BAAiB,mBAAU,oCAAqC,8BAAhEA,2CAEJT,UAAUE,UAAYO,QACtBX,KAAKY,UAAUC,OAAO,mDACJ,WAAXhB,sCACPK,UAAUE,UAAY,qBACMU,mBAAUC,OAAO,sCAAuC,YAApFb,UAAUE,0DACQ,kBAAXP,sCACDmB,cAAgBhB,KAAKiB,cAAc,0BAA0BC,MAAMC,WAAWC,OACpFlB,UAAUE,UAAY,IAChBI,KAAO,CACTC,OAAQN,OACRkB,OAAQL,eAAgC,uBAElBX,cAAKC,KAAK,CAAC,CACjCC,WAAY,wCACZC,KAAMA,QACN,kBAHEE,6CAKc,mBAAU,oBAAqB,2DAA/CC,qBAAuE,KACvED,wDACAC,wCAAuBD,qCACX,mBAAU,mCAAoC,6FAD1DC,mCAEM,0DAENA,2BAAiB,mBAAU,oCAAqC,8BAAhEA,6CAEJT,UAAUE,UAAYO,2CACJ,kBAAXd,sCACPK,UAAUE,UAAY,qBACMU,mBAAUC,OAAO,oCAAqC,YAAlFb,UAAUE"} \ 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', '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,gBACLC,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"} + diff --git a/amd/build/warnmodechange.min.js.map b/amd/build/warnmodechange.min.js.map index f3c76304c1..80beade8a2 100644 --- a/amd/build/warnmodechange.min.js.map +++ b/amd/build/warnmodechange.min.js.map @@ -1 +1,2 @@ -{"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","e","value","selectedOptions","preventDefault","Notification","undefined","requestSubmit","submitter","confirm"],"mappings":"mhBA8BqBA,mCACRC,gBAAgB,qBAAsB,CAAC,mBAAoB,mCAC3DA,gBAAgB,SAAU,CAAC,UAAW,eACzCC,KAAOC,SAASC,cAAc,cAC9BC,OAASF,SAASG,eAAe,qBACvCJ,KAAKK,sCAAW,iBAAMC,sHACZC,MAAQJ,OAAOK,gBAAgB,GAAGD,QAC3BT,iBAA4B,GAATS,OAAuB,GAATA,4EAG9CD,EAAEG,6BACIC,uCACI,mBAAU,qEACV,mBAAmB,GAATH,MAAa,mBAAqB,eAAgB,iFAC5D,mBAAU,sEACV,mBAAU,+DAChB,WAEIP,KAAKK,cAAWM,EAChBX,KAAKY,cAAcN,EAAEO,6BACtBF,+BATYG"} \ 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 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,gBACNC,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,GATP"} + diff --git a/amd/src/rating.js b/amd/src/rating.js index 7d504d76c7..41ac768c30 100644 --- a/amd/src/rating.js +++ b/amd/src/rating.js @@ -65,8 +65,10 @@ async function sendVote(postid, rating, userid) { * Init function. * * @param {int} userid + * @param {boolean} allowmultiplemarks // true means allowed, false means not allowed. + * */ -export function init(userid) { +export function init(userid, allowmultiplemarks) { Prefetch.prefetchStrings('mod_moodleoverflow', ['marksolved', 'marknotsolved', 'markhelpful', 'marknothelpful', 'action_remove_upvote', 'action_upvote', 'action_remove_downvote', 'action_downvote']); @@ -109,17 +111,70 @@ export function init(userid) { const baseRating = isHelpful ? RATING_HELPFUL : RATING_SOLVED; const rating = shouldRemove ? baseRating * 10 : baseRating; await sendVote(postid, rating, userid); - for (const el of root.querySelectorAll('.moodleoverflowpost.' + htmlclass)) { - el.classList.remove(htmlclass); - el.querySelector(`[data-moodleoverflow-action="${action}"]`).textContent = - await getString(`mark${action}`, 'mod_moodleoverflow'); + + /* If multiplemarks are not allowed (that is the default mode): delete all marks. + else: only delete the mark if the post is being unmarked. + + then add a mark, if the post is being marked. + */ + if (!allowmultiplemarks) { + // Delete all marks in the discussion + for (const el of root.querySelectorAll('.moodleoverflowpost.' + htmlclass)) { + el.classList.remove(htmlclass); + el.querySelector(`[data-moodleoverflow-action="${action}"]`).textContent = + await getString(`mark${action}`, 'mod_moodleoverflow'); + } + } else { + // Remove only the mark of the unmarked post. + if (shouldRemove) { + postElement.classList.remove(htmlclass); + actionElement.textContent = await getString(`mark${action}`, 'mod_moodleoverflow'); + changeStrings(htmlclass, action); + } } + // If the post is being marked, mark it. if (!shouldRemove) { postElement.classList.add(htmlclass); actionElement.textContent = await getString(`marknot${action}`, 'mod_moodleoverflow'); + if (allowmultiplemarks) { + changeStrings(htmlclass, action); + } } + } } }; +} + +/** + * Function to change the String of the post data-action button. + * Only usable if mulitplemarks are allowed. + * @param {string} htmlclass the class where the String is being updated + * @param {string} action helpful or solved mark + */ +async function changeStrings(htmlclass, action) { + Prefetch.prefetchStrings('mod_moodleoverflow', + ['marksolved', 'alsomarksolved', 'markhelpful', 'alsomarkhelpful',]); + + // 1. Step: Are there other posts in the Discussion, that are solved/helpful? + var othermarkedposts = false; + for (const el of root.querySelectorAll('.moodleoverflowpost')) { + if (el.classList.contains(htmlclass)) { + othermarkedposts = true; + break; + } + } + // 2. Step: Change the strings of the action Button of the unmarked posts. + for (const el of root.querySelectorAll('.moodleoverflowpost')) { + if (!el.classList.contains(htmlclass) && el.querySelector(`[data-moodleoverflow-action="${action}"]`)) { + if (othermarkedposts) { + el.querySelector(`[data-moodleoverflow-action="${action}"]`).textContent = + await getString(`alsomark${action}`, 'mod_moodleoverflow'); + } else { + el.querySelector(`[data-moodleoverflow-action="${action}"]`).textContent = + await getString(`mark${action}`, 'mod_moodleoverflow'); + } + } + } } \ No newline at end of file diff --git a/classes/capabilities.php b/classes/capabilities.php index be17015033..44dda716d2 100644 --- a/classes/capabilities.php +++ b/classes/capabilities.php @@ -35,22 +35,57 @@ */ class capabilities { + /** capability */ const ADD_INSTANCE = 'mod/moodleoverflow:addinstance'; + + /** capability */ const VIEW_DISCUSSION = 'mod/moodleoverflow:viewdiscussion'; + + /** capability */ const REPLY_POST = 'mod/moodleoverflow:replypost'; + + /** capability */ const START_DISCUSSION = 'mod/moodleoverflow:startdiscussion'; + + /** capability */ const EDIT_ANY_POST = 'mod/moodleoverflow:editanypost'; + + /** capability */ const DELETE_OWN_POST = 'mod/moodleoverflow:deleteownpost'; + + /** capability */ const DELETE_ANY_POST = 'mod/moodleoverflow:deleteanypost'; + + /** capability */ const RATE_POST = 'mod/moodleoverflow:ratepost'; + + /** capability */ const MARK_SOLVED = 'mod/moodleoverflow:marksolved'; + + /** capability */ const MANAGE_SUBSCRIPTIONS = 'mod/moodleoverflow:managesubscriptions'; + + /** capability */ const ALLOW_FORCE_SUBSCRIBE = 'mod/moodleoverflow:allowforcesubscribe'; + + /** capability */ const CREATE_ATTACHMENT = 'mod/moodleoverflow:createattachment'; + + /** capability */ const REVIEW_POST = 'mod/moodleoverflow:reviewpost'; + /** @var array */ private static $cache = []; + /** + * Saves the cache from has_capability. + * + * @param string $capability The capability that is being checked. + * @param context $context The context. + * @param int|null $userid The user ID. + * + * @return bool true or false + */ public static function has(string $capability, context $context, $userid = null): bool { global $USER; if (!$userid) { diff --git a/classes/ratings.php b/classes/ratings.php index 899755d6d1..4352edc404 100644 --- a/classes/ratings.php +++ b/classes/ratings.php @@ -77,6 +77,13 @@ public static function moodleoverflow_add_rating($moodleoverflow, $postid, $rati throw new moodle_exception('invalidcourseid'); } + // Are multiple marks allowed? + $markssetting = $DB->get_record('moodleoverflow', array('id' => $moodleoverflow->id), 'allowmultiplemarks'); + $multiplemarks = false; + if ($markssetting->allowmultiplemarks == 1) { + $multiplemarks = true; + } + // Retrieve the contexts. $modulecontext = \context_module::instance($cm->id); $coursecontext = \context_course::instance($course->id); @@ -138,18 +145,29 @@ public static function moodleoverflow_add_rating($moodleoverflow, $postid, $rati throw new moodle_exception('notteacher', 'moodleoverflow'); } - // Get other ratings in the discussion. - $sql = "SELECT * - FROM {moodleoverflow_ratings} - WHERE discussionid = ? AND rating = ?"; - $otherrating = $DB->get_record_sql($sql, [ $discussion->id, $rating ]); + // Check if multiple marks are not enabled. + if (!$multiplemarks) { + + // Get other ratings in the discussion. + $sql = "SELECT * + FROM {moodleoverflow_ratings} + WHERE discussionid = ? AND rating = ?"; + $otherrating = $DB->get_record_sql($sql, [ $discussion->id, $rating ]); + + // If there is an old rating, update it. Else create a new rating record. + if ($otherrating) { + return self::moodleoverflow_update_rating_record($post->id, $rating, $userid, $otherrating->id, $modulecontext); + + } else { + $mid = $moodleoverflow->id; + + return self::moodleoverflow_add_rating_record($mid, $discussion->id, $post->id, + $rating, $userid, $modulecontext); + } - // If there is an old rating, update it. Else create a new rating record. - if ($otherrating) { - return self::moodleoverflow_update_rating_record($post->id, $rating, $userid, $otherrating->id, $modulecontext); } else { + // If multiplemarks are allowed, only create a new rating. $mid = $moodleoverflow->id; - return self::moodleoverflow_add_rating_record($mid, $discussion->id, $post->id, $rating, $userid, $modulecontext); } } @@ -247,56 +265,96 @@ public static function moodleoverflow_sort_answers_by_ratings($posts) { $statusstarter = self::moodleoverflow_discussion_is_solved($discussionid, false); $statusteacher = self::moodleoverflow_discussion_is_solved($discussionid, true); - // The answer that is marked as correct by both is displayed first. + // The answers that are marked as correct by both are displayed first. if ($statusteacher && $statusstarter) { + $markedposts = array(); + foreach ($statusteacher as $solvedposts) { + foreach ($statusstarter as $helpfulposts) { + // Is the same answer correct for both? + if ($solvedposts->postid == $helpfulposts->postid) { + // Save the post that is marked as solved and helpful and go to the next post. + $markedposts[] = $postscopy[$solvedposts->postid]; + break; + } + } + } + // Now sort the posts by their votes. + self::moodleoverflow_sort_post_by_votes($markedposts); - // Is the same answer correct for both? - if ($statusstarter->postid == $statusteacher->postid) { - + // Iterate trough the marked posts and add it to the new Order. + foreach ($markedposts as $post) { // Add the post to the new order and delete it from the posts array. - $neworder[] = (int) $statusstarter->postid; - unset($postscopy[$statusstarter->postid]); - - // Unset the stati to skip the following if-statements. - $statusstarter = false; - $statusteacher = false; + $neworder[] = (int) $post->id; + unset($postscopy[$post->id]); } } // If the answers the teacher marks are preferred, and only // the teacher marked an answer as solved, display it first. if ($preferteacher && $statusteacher) { + // Save the marked posts. + $markedposts = array(); + foreach ($statusteacher as $solvedpost) { + if (array_key_exists($solvedpost->postid, $postscopy)) { + $markedposts[] = $postscopy[$solvedpost->postid]; + } + } + // Sort the solved answers. + self::moodleoverflow_sort_post_by_votes($markedposts); - // Add the post to the new order and delete it from the posts array. - $neworder[] = (int) $statusteacher->postid; - unset($postscopy[$statusteacher->postid]); - - // Unset the status to skip the following if-statements. - $statusteacher = false; + // Iterate trough the marked posts and add it to the new Order. + foreach ($markedposts as $post) { + // Add the post to the new order and delete it from the posts array. + $neworder[] = (int) $post->id; + unset($postscopy[$post->id]); + } } // If the user who started the discussion has marked // an answer as helpful, display this answer first. if ($statusstarter) { + // Save the marked posts. + $markedposts = array(); + foreach ($statusstarter as $helpfulpost) { + if (array_key_exists($helpfulpost->postid, $postscopy)) { + $markedposts[] = $postscopy[$helpfulpost->postid]; + } + } + // Sort the helpful answers. + self::moodleoverflow_sort_post_by_votes($markedposts); - // Add the post to the new order and delete it from the posts array. - $neworder[] = (int) $statusstarter->postid; - unset($postscopy[$statusstarter->postid]); + // Iterate trough the marked posts and add it to the new Order. + foreach ($markedposts as $post) { + // Add the post to the new order and delete it from the posts array. + $neworder[] = (int) $post->id; + unset($postscopy[$post->id]); + } } // If a teacher has marked an answer as solved, display it next. if ($statusteacher) { + // Save the marked posts. + $markedposts = array(); + foreach ($statusteacher as $solvedpost) { + if (array_key_exists($solvedpost->postid, $postscopy)) { + $markedposts[] = $postscopy[$solvedpost->postid]; + } + } + // Sort the solved answers. + self::moodleoverflow_sort_post_by_votes($markedposts); - // Add the post to the new order and delete it from the posts array. - $neworder[] = (int) $statusteacher->postid; - unset($postscopy[$statusteacher->postid]); + // Iterate trough the marked posts and add it to the new Order. + foreach ($markedposts as $post) { + // Add the post to the new order and delete it from the posts array. + $neworder[] = (int) $post->id; + unset($postscopy[$post->id]); + } } // All answers that are not marked by someone should now be left. // Search for all comments. foreach ($postscopy as $postid => $post) { - // Add all comments to the order. // They are independant from the votes. if ($post->parent != $parent->id) { @@ -426,8 +484,8 @@ public static function moodleoverflow_discussion_is_solved($discussionid, $teach // Check if a teacher marked a solution as solved. if ($DB->record_exists('moodleoverflow_ratings', array('discussionid' => $discussionid, 'rating' => 3))) { - // Return the rating record. - return $DB->get_record('moodleoverflow_ratings', array('discussionid' => $discussionid, 'rating' => 3)); + // Return the rating records. + return $DB->get_records('moodleoverflow_ratings', array('discussionid' => $discussionid, 'rating' => 3)); } // The teacher has not marked the discussion as solved. @@ -437,8 +495,8 @@ public static function moodleoverflow_discussion_is_solved($discussionid, $teach // Check if the topic starter marked a solution as helpful. if ($DB->record_exists('moodleoverflow_ratings', array('discussionid' => $discussionid, 'rating' => 4))) { - // Return the rating record. - return $DB->get_record('moodleoverflow_ratings', array('discussionid' => $discussionid, 'rating' => 4)); + // Return the rating records. + return $DB->get_records('moodleoverflow_ratings', array('discussionid' => $discussionid, 'rating' => 4)); } // The topic starter has not marked a solution as helpful. @@ -787,4 +845,51 @@ public static function moodleoverflow_user_can_rate($post, $modulecontext, $user && $post->reviewed == 1; } + /** + * Function that sorts an array of posts. + * + * @param array $posts The posts objects that are being sorted. + */ + private static function moodleoverflow_sort_post_by_votes(array $posts) { + // Function uses quicksort to sort the posts in descending order. + self::moodleoverflow_quicksort_post_by_votes($posts, 0, count($posts) - 1); + } + + /** + * Sorts array of posts with quicksort algorithm. + * + * @param array $posts The posts that are being sorted by quicksort. + * @param int $low Index for quicksort. + * @param int $high Index for quicksort. + */ + private static function moodleoverflow_quicksort_post_by_votes(array $posts, $low, $high) { + if ($low >= $high) { + return; + } + $left = $low; + $right = $high; + $pivot = $posts[($low + $high) / 2]->votesdifference; + do { + while ($posts[$left]->votesdifference > $pivot) { + $left++; + } + while ($posts[$right]->votesdifference < $pivot) { + $right--; + } + if ($left <= $right) { + $temp = $posts[$right]; + $post[$right] = $posts[$left]; + $posts[$left] = $temp; + $right--; + $left++; + } + } while ($left <= $right); + if ($low < $right) { + self::moodleoverflow_quicksort_post_by_votes($posts, $low, $right); + } + if ($high > $left ) { + self::moodleoverflow_quicksort_post_by_votes($posts, $left, $high); + } + } + } diff --git a/classes/tables/userstats_table.php b/classes/tables/userstats_table.php index 4883aeb58b..5d040b1990 100644 --- a/classes/tables/userstats_table.php +++ b/classes/tables/userstats_table.php @@ -39,14 +39,26 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class userstats_table extends \flexible_table { - private $courseid; // Course ID. - private $moodleoverflowid; // Moodleoverflow that started the printing of statistics. - private $userstatsdata = array(); // Userstatsdata is a table that will have objects with every user and his statistics. - private $helpactivity; // Help icon for amountofactivity-column. + + /** @var int the Course ID*/ + private $courseid; + + /** @var int Moodleoverflow that started the printing of statistics*/ + private $moodleoverflowid; + + /** @var array table that will have objects with every user and his statistics. */ + private $userstatsdata = array(); + + /** @var \stdClass Help icon for amountofactivity-column.*/ + private $helpactivity; /** * Constructor for workflow_table. + * * @param int $uniqueid Unique id of this table. + * @param int $courseid + * @param int $moodleoverflow ID if the moodleoverflow + * @param string $url The url of the table */ public function __construct($uniqueid, $courseid, $moodleoverflow, $url) { global $PAGE; @@ -87,6 +99,10 @@ public function out() { /** * Method to sort the userstatsdata-table. + * + * @param array $sortorder The sort order array. + * + * @return void */ private function sort_table_data($sortorder) { $key = $sortorder['sortby']; @@ -103,6 +119,13 @@ private function sort_table_data($sortorder) { /** * Sorts userstatsdata with quicksort algorithm. + * + * @param int $low index for quicksort. + * @param int $high index for quicksort. + * @param int $key the column that is being sorted (upvotes, downvotes etc.). + * @param string $order sort in ascending or descending order. + * + * @return void */ private function quick_usertable_sort($low, $high, $key, $order) { if ($low >= $high) { @@ -256,10 +279,21 @@ public function set_helpactivity() { } // Functions that show the data. + + /** + * username column + * @param object $row + * @return string + */ public function col_username($row) { return $row->link; } + /** + * upvotes column + * @param object $row + * @return string + */ public function col_receivedupvotes($row) { if ($row->receivedupvotes > 0) { return \html_writer::tag('h5', \html_writer::start_span('badge badge-success') . @@ -270,6 +304,11 @@ public function col_receivedupvotes($row) { } } + /** + * downvotes column + * @param object $row + * @return string + */ public function col_receiveddownvotes($row) { if ($row->receiveddownvotes > 0) { return \html_writer::tag('h5', \html_writer::start_span('badge badge-success') . @@ -280,6 +319,11 @@ public function col_receiveddownvotes($row) { } } + /** + * activity column + * @param object $row + * @return string + */ public function col_activity($row) { if ($row->activity > 0) { return \html_writer::tag('h5', \html_writer::start_span('badge badge-success') . @@ -290,6 +334,11 @@ public function col_activity($row) { } } + /** + * reputation column + * @param object $row + * @return string + */ public function col_reputation($row) { if ($row->reputation > 0) { return \html_writer::tag('h5', \html_writer::start_span('badge badge-success') . @@ -300,6 +349,12 @@ public function col_reputation($row) { } } + /** + * error handling + * @param object $colname + * @param int $attempt + * @return null + */ public function other_cols($colname, $attempt) { return null; } diff --git a/classes/task/send_mails.php b/classes/task/send_mails.php index 7417ee6a38..268774bc29 100644 --- a/classes/task/send_mails.php +++ b/classes/task/send_mails.php @@ -67,7 +67,7 @@ public function execute() { * Sends initial notifications for needed reviews to all users with review capability. */ public function send_review_notifications() { - global $DB, $OUTPUT, $PAGE; + global $DB, $OUTPUT, $PAGE, $CFG; $rendererhtml = $PAGE->get_renderer('mod_moodleoverflow', 'email', 'htmlemail'); $renderertext = $PAGE->get_renderer('mod_moodleoverflow', 'email', 'textemail'); @@ -128,7 +128,12 @@ public function send_review_notifications() { foreach ($usersto as $userto) { try { - cron_setup_user($userto, $course); + // Check for moodle version. Version 401 supported until 8 December 2025. + if ($CFG->branch >= 402) { + \core\cron::setup_user($userto, $course); + } else { + cron_setup_user($userto, $course); + } $maildata = new moodleoverflow_email( $course, diff --git a/db/access.php b/db/access.php index 4faa51067c..bf56c9400f 100644 --- a/db/access.php +++ b/db/access.php @@ -111,11 +111,11 @@ ), 'mod/moodleoverflow:deleteownpost' => array( - 'captype' => 'write', - 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( - 'student' => CAP_ALLOW, - 'teacher' => CAP_ALLOW, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), @@ -123,10 +123,10 @@ ), 'mod/moodleoverflow:deleteanypost' => array( - 'captype' => 'write', - 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( - 'teacher' => CAP_ALLOW, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), @@ -158,10 +158,10 @@ 'mod/moodleoverflow:managesubscriptions' => array( 'riskbitmask' => RISK_SPAM, - 'captype' => 'write', - 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( - 'teacher' => CAP_ALLOW, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), diff --git a/db/install.xml b/db/install.xml index fb1f1f2b2e..e4651fdc18 100644 --- a/db/install.xml +++ b/db/install.xml @@ -27,6 +27,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index 8c5b7f2356..1abb5af03e 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -273,5 +273,22 @@ function xmldb_moodleoverflow_upgrade($oldversion) { // Moodleoverflow savepoint reached. upgrade_mod_savepoint(true, 2023022400, 'moodleoverflow'); } + + if ($oldversion < 2023040400) { + // Define table moodleoverflow to be edited. + $table = new xmldb_table('moodleoverflow'); + + // Define field allowmultiplemarks to be added to moodleoverflow. + $field = new xmldb_field('allowmultiplemarks', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'needsreview'); + + // Conditionally launch add field allowmultiplemarks. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Moodleoverflow savepoint reached. + upgrade_mod_savepoint(true, 2023040400, 'moodleoverflow'); + } + return true; } diff --git a/discussion.php b/discussion.php index e0af3e24c4..54014664fa 100644 --- a/discussion.php +++ b/discussion.php @@ -50,6 +50,13 @@ throw new moodle_exception('invalidcourseid'); } +// Save the allowmultiplemarks setting. +$marksetting = $DB->get_record('moodleoverflow', array('id' => $moodleoverflow->id), 'allowmultiplemarks'); +$multiplemarks = false; +if ($marksetting->allowmultiplemarks == 1) { + $multiplemarks = true; +} + // Get the related coursemodule and its context. if (!$cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflow->id, $course->id)) { throw new moodle_exception('invalidcoursemodule'); @@ -128,7 +135,7 @@ $PAGE->requires->js_call_amd('mod_moodleoverflow/reviewing', 'init'); -$PAGE->requires->js_call_amd('mod_moodleoverflow/rating', 'init', [$USER->id]); +$PAGE->requires->js_call_amd('mod_moodleoverflow/rating', 'init', [$USER->id, $multiplemarks]); // Initiate the page. $PAGE->set_title($course->shortname . ': ' . format_string($discussion->name)); @@ -150,7 +157,7 @@ echo '
'; -moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discussion, $post); +moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discussion, $post, $multiplemarks); echo '
'; diff --git a/externallib.php b/externallib.php index 66df2d88b8..a7c29fb75f 100644 --- a/externallib.php +++ b/externallib.php @@ -46,8 +46,8 @@ class mod_moodleoverflow_external extends external_api { public static function record_vote_parameters() { return new external_function_parameters( array( - 'postid' => new external_value(PARAM_INT, 'id of post'), - 'ratingid' => new external_value(PARAM_INT, 'rating') + 'postid' => new external_value(PARAM_INT, 'id of post'), + 'ratingid' => new external_value(PARAM_INT, 'rating') ) ); } @@ -79,8 +79,8 @@ public static function record_vote($postid, $ratingid) { // Parameter validation. $params = self::validate_parameters(self::record_vote_parameters(), array( - 'postid' => $postid, - 'ratingid' => $ratingid, + 'postid' => $postid, + 'ratingid' => $ratingid, )); $transaction = $DB->start_delegated_transaction(); diff --git a/lang/en/moodleoverflow.php b/lang/en/moodleoverflow.php index 871a4ded87..13d0e76ec1 100644 --- a/lang/en/moodleoverflow.php +++ b/lang/en/moodleoverflow.php @@ -188,8 +188,10 @@ $string['ratingfailed'] = 'Rating failed. Try again.'; $string['rateownpost'] = 'You cannot rate your own post.'; $string['marksolved'] = 'Mark as solution'; +$string['alsomarksolved'] = "Also mark as solution"; $string['marknotsolved'] = 'Remove solution mark'; -$string['markhelpful'] = 'Mark as Helpful'; +$string['markhelpful'] = 'Mark as helpful'; +$string['alsomarkhelpful'] = "Also mark as helpful"; $string['marknothelpful'] = 'Not Helpful'; $string['answer'] = '{$a} Answer'; $string['answers'] = '{$a} Answers'; @@ -334,6 +336,8 @@ $string['attachment'] = 'Attachment'; $string['attachments'] = 'Attachments'; $string['attachment_help'] = 'You can optionally attach one or more files to a forum post. If you attach an image, it will be displayed after the message.'; +$string['allowmultiplemarks'] = 'Multiple marks?'; +$string['allowmultiplemarks_help'] = 'A post can be marked as helpful or solved. Within a discussion, only one post can be marked as helpful/solved. Click the checkbox to mark multiple posts as helpful/solved.'; // Templates. $string['reputation'] = 'Reputation'; diff --git a/lib.php b/lib.php index 980856a25f..5ee67061d7 100644 --- a/lib.php +++ b/lib.php @@ -749,7 +749,12 @@ function moodleoverflow_send_mails() { $userto->markposts = array(); // Cache the capabilities of the user. - cron_setup_user($userto); + // Check for moodle version. Version 401 supported until 8 December 2025. + if ($CFG->branch >= 402) { + \core\cron::setup_user($userto); + } else { + cron_setup_user($userto); + } // Reset the caches. foreach ($coursemodules as $moodleoverflowid => $unused) { @@ -838,7 +843,12 @@ function moodleoverflow_send_mails() { } // Setup roles and languages. - cron_setup_user($userto, $course); + // Check for moodle version. Version 401 supported until 8 December 2025. + if ($CFG->branch >= 402) { + \core\cron::setup_user($userto, $course); + } else { + cron_setup_user($userto, $course); + } // Cache the users capability to view full names. if (!isset($userto->viewfullnames[$moodleoverflow->id])) { diff --git a/locallib.php b/locallib.php index b120deaa22..fc6b5d4537 100644 --- a/locallib.php +++ b/locallib.php @@ -269,6 +269,8 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = - $preparedarray[$i]['starterlink'] = null; if ($statusstarter) { $link = '/mod/moodleoverflow/discussion.php?d='; + $statusstarter = $statusstarter[array_key_first($statusstarter)]; + $preparedarray[$i]['starterlink'] = new moodle_url($link . $statusstarter->discussionid . '#p' . $statusstarter->postid); } @@ -278,6 +280,8 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = - $preparedarray[$i]['teacherlink'] = null; if ($statusteacher) { $link = '/mod/moodleoverflow/discussion.php?d='; + $statusteacher = $statusteacher[array_key_first($statusteacher)]; + $preparedarray[$i]['teacherlink'] = new moodle_url($link . $statusteacher->discussionid . '#p' . $statusteacher->postid); } @@ -328,7 +332,8 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = - } } else { // Get his picture, his name and the link to his profile. - $preparedarray[$i]['picture'] = $OUTPUT->user_picture($startuser, array('courseid' => $moodleoverflow->course, 'link' => false)); + $preparedarray[$i]['picture'] = $OUTPUT->user_picture($startuser, array('courseid' => $moodleoverflow->course, + 'link' => false)); $preparedarray[$i]['username'] = fullname($startuser, has_capability('moodle/site:viewfullnames', $context)); $preparedarray[$i]['userlink'] = $CFG->wwwroot . '/user/view.php?id=' . $discussion->userid . '&course=' . $moodleoverflow->course; @@ -443,6 +448,10 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = - /** * Prints a popup with a menu of other moodleoverflow in the course. * Menu to move a topic to another moodleoverflow forum. + * + * @param object $course + * @param object $cm + * @param int $movetopopup forum where forum list is being printed. */ function moodleoverflow_print_forum_list($course, $cm, $movetopopup) { global $CFG, $DB, $PAGE; @@ -480,6 +489,7 @@ function moodleoverflow_print_forum_list($course, $cm, $movetopopup) { echo $renderer->render_forum_list($mustachedata); } + /** * Returns an array of counts of replies for each discussion. * @@ -911,10 +921,10 @@ function moodleoverflow_user_can_post($modulecontext, $posttoreplyto, $considerr * @param stdClass $moodleoverflow The moodleoverflow object * @param stdClass $discussion The discussion object * @param stdClass $post The post object + * @param bool $multiplemarks The setting of allowmultiplemarks */ -function moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discussion, $post) { +function moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discussion, $post, $multiplemarks = false) { global $USER; - // Check if the current is the starter of the discussion. $ownpost = (isloggedin() && ($USER->id == $post->userid)); @@ -966,7 +976,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); + $ownpost, false, '', '', $postread, true, $istracked, 0, $usermapping, 0, $multiplemarks); // Print answer divider. if ($answercount == 1) { @@ -980,7 +990,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); + null, $usermapping, $multiplemarks); echo ''; } @@ -1049,6 +1059,7 @@ function moodleoverflow_get_all_discussion_posts($discussionid, $tracking, $modc // Assign the ratings to the matching posts. $posts[$postid]->upvotes = $discussionratings[$post->id]->upvotes; $posts[$postid]->downvotes = $discussionratings[$post->id]->downvotes; + $posts[$postid]->votesdifference = $posts[$postid]->upvotes - $posts[$postid]->downvotes; $posts[$postid]->statusstarter = $discussionratings[$post->id]->ishelpful; $posts[$postid]->statusteacher = $discussionratings[$post->id]->issolved; } @@ -1108,6 +1119,7 @@ function moodleoverflow_get_all_discussion_posts($discussionid, $tracking, $modc * @param bool $iscomment * @param array $usermapping * @param int $level + * @param bool $multiplemarks The Setting of allowmultiplemarks * @return void|null * @throws coding_exception * @throws dml_exception @@ -1117,7 +1129,7 @@ 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) { + $iscomment = false, $usermapping = [], $level = 0, $multiplemarks = false) { global $USER, $CFG, $OUTPUT, $PAGE; // Require the filelib. @@ -1174,8 +1186,10 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co $str->markread = get_string('markread', 'moodleoverflow'); $str->markunread = get_string('markunread', 'moodleoverflow'); $str->marksolved = get_string('marksolved', 'moodleoverflow'); + $str->alsomarksolved = get_string('alsomarksolved', 'moodleoverflow'); $str->marknotsolved = get_string('marknotsolved', 'moodleoverflow'); $str->markhelpful = get_string('markhelpful', 'moodleoverflow'); + $str->alsomarkhelpful = get_string('alsomarkhelpful', 'moodleoverflow'); $str->marknothelpful = get_string('marknothelpful', 'moodleoverflow'); } @@ -1214,24 +1228,38 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co $permalink = new moodle_url($discussionlink); $permalink->set_anchor('p' . $post->id); + // Check if multiplemarks are allowed, if so, check if there are already marked posts. + $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); + } + // If the user has started the discussion, he can mark the answer as helpful. $canmarkhelpful = (($USER->id == $discussion->userid) && ($USER->id != $post->userid) && ($iscomment != $post->parent) && !empty($post->parent)); if ($canmarkhelpful) { - // When the post is already marked, remove the mark instead. $link = '/mod/moodleoverflow/discussion.php'; if ($post->statusstarter) { $commands[] = html_writer::tag('a', $str->marknothelpful, array('class' => 'markhelpful onlyifreviewed', 'role' => 'button', 'data-moodleoverflow-action' => 'helpful')); } else { - $commands[] = html_writer::tag('a', $str->markhelpful, + // If there are already marked posts, change the string of the button. + if ($helpfulposts) { + $commands[] = html_writer::tag('a', $str->alsomarkhelpful, + array('class' => 'markhelpful onlyifreviewed', 'role' => 'button', 'data-moodleoverflow-action' => 'helpful')); + } else { + $commands[] = html_writer::tag('a', $str->markhelpful, array('class' => 'markhelpful onlyifreviewed', 'role' => 'button', 'data-moodleoverflow-action' => 'helpful')); + } } } // A teacher can mark an answer as solved. - $canmarksolved = (($iscomment != $post->parent) AND !empty($post->parent) AND capabilities::has(capabilities::MARK_SOLVED, $modulecontext)); + $canmarksolved = (($iscomment != $post->parent) && !empty($post->parent) + && capabilities::has(capabilities::MARK_SOLVED, $modulecontext)); if ($canmarksolved) { // When the post is already marked, remove the mark instead. @@ -1240,8 +1268,14 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co $commands[] = html_writer::tag('a', $str->marknotsolved, array('class' => 'marksolved onlyifreviewed', 'role' => 'button', 'data-moodleoverflow-action' => 'solved')); } else { - $commands[] = html_writer::tag('a', $str->marksolved, + // If there are already marked posts, change the string of the button. + if ($solvedposts) { + $commands[] = html_writer::tag('a', $str->alsomarksolved, array('class' => 'marksolved onlyifreviewed', 'role' => 'button', 'data-moodleoverflow-action' => 'solved')); + } else { + $commands[] = html_writer::tag('a', $str->marksolved, + array('class' => 'marksolved onlyifreviewed', 'role' => 'button', 'data-moodleoverflow-action' => 'solved')); + } } } @@ -1296,7 +1330,7 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co $mustachedata->isread = false; $mustachedata->isfirstunread = false; $mustachedata->isfirstpost = false; - $mustachedata->iscomment = (!empty($post->parent) AND ($iscomment == $post->parent)); + $mustachedata->iscomment = (!empty($post->parent) && ($iscomment == $post->parent)); $mustachedata->permalink = $permalink; // Get the ratings. @@ -1461,14 +1495,15 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co * @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 array $usermapping + * @param bool $multiplemarks The Setting of allowmultiplemarks * @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 = []) { + $istracked, $posts, $iscomment = null, $usermapping = [], $multiplemarks = false) { global $USER; // Prepare the output. @@ -1510,11 +1545,11 @@ function moodleoverflow_print_posts_nested($course, &$cm, $moodleoverflow, $disc // Print the answer. $output .= moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $course, - $ownpost, false, '', '', $postread, true, $istracked, $parentid, $usermapping, $level); + $ownpost, false, '', '', $postread, true, $istracked, $parentid, $usermapping, $level, $multiplemarks); // Print its children. $output .= moodleoverflow_print_posts_nested($course, $cm, $moodleoverflow, - $discussion, $post, $istracked, $posts, $parentid, $usermapping); + $discussion, $post, $istracked, $posts, $parentid, $usermapping, $multiplemarks); // End the div. $output .= "\n"; diff --git a/mod_form.php b/mod_form.php index 1cb85ae630..4df07d8537 100644 --- a/mod_form.php +++ b/mod_form.php @@ -224,6 +224,11 @@ public function definition() { $mform->addHelpButton('allownegativereputation', 'allownegativereputation', 'moodleoverflow'); $mform->setDefault('allownegativereputation', MOODLEOVERFLOW_REPUTATION_NEGATIVE); + // Allow multiple marks of helpful/solved. + $mform->addElement('advcheckbox', 'allowmultiplemarks', get_string('allowmultiplemarks', 'moodleoverflow')); + $mform->addHelpButton('allowmultiplemarks', 'allowmultiplemarks', 'moodleoverflow'); + $mform->setDefault('allowmultiplemarks', 0); + // Add standard elements, common to all modules. $this->standard_coursemodule_elements(); diff --git a/renderer.php b/renderer.php index f6458b547c..905bc3d12d 100644 --- a/renderer.php +++ b/renderer.php @@ -49,6 +49,10 @@ public function render_discussion_list($data) { /** * Display the forum list in the view.php if a discussion needs to be moved to another forum. + * + * @param object $data The prepared variables. + * + * @return string */ public function render_forum_list($data) { return $this->render_from_template('mod_moodleoverflow/forum_list', $data); diff --git a/tests/behat/behat_mod_moodleoverflow.php b/tests/behat/behat_mod_moodleoverflow.php index 7d1375c6c1..0f21dd17f0 100644 --- a/tests/behat/behat_mod_moodleoverflow.php +++ b/tests/behat/behat_mod_moodleoverflow.php @@ -113,6 +113,10 @@ protected function add_new_discussion($moodleoverflowname, TableNode $table, $bu $this->execute('behat_general::i_wait_to_be_redirected'); } + /** + * Gets the container node. + * @param string $discussiontitle + */ protected function find_moodleoverflow_discussion_card(string $discussiontitle): \Behat\Mink\Element\Element { return $this->find('xpath', '//*[contains(concat(" ",normalize-space(@class)," ")," moodleoverflowdiscussion ")][.//*[text()="'. @@ -124,7 +128,8 @@ protected function find_moodleoverflow_discussion_card(string $discussiontitle): * * This step is for advanced users, use it if you don't find anything else suitable for what you need. * - * @Then /^"(?P(?:[^"]|\\")*)" "(?P[^"]*)" should exist in the "(?P(?:[^"]|\\")*)" moodleoverflow discussion card$/ + * @Then /^"(?P(?:[^"]|\\")*)" "(?P[^"]*)" should exist in the + * "(?P(?:[^"]|\\")*)" moodleoverflow discussion card$/ * @throws ElementNotFoundException Thrown by behat_base::find * @param string $element The locator of the specified selector * @param string $selectortype The selector type @@ -145,7 +150,8 @@ public function should_exist_in_the_moodleoverflow_discussion_card($element, $se /** * Click on the element of the specified type which is located inside the second element. * - * @When /^I click on "(?P(?:[^"]|\\")*)" "(?P[^"]*)" in the "(?P(?:[^"]|\\")*)" moodleoverflow discussion card$/ + * @When /^I click on "(?P(?:[^"]|\\")*)" "(?P[^"]*)" in the + * "(?P(?:[^"]|\\")*)" moodleoverflow discussion card$/ * @param string $element Element we look for * @param string $selectortype The type of what we look for * @param string $discussiontitle The discussion title @@ -169,7 +175,8 @@ public function i_click_on_in_the_moodleoverflow_discussion_card($element, $sele * * This step is for advanced users, use it if you don't find anything else suitable for what you need. * - * @Then /^"(?P(?:[^"]|\\")*)" "(?P[^"]*)" should not exist in the "(?P(?:[^"]|\\")*)" moodleoverflow discussion card$/ + * @Then /^"(?P(?:[^"]|\\")*)" "(?P[^"]*)" should not exist in the + * "(?P(?:[^"]|\\")*)" moodleoverflow discussion card$/ * @throws ExpectationException * @param string $element The locator of the specified selector * @param string $selectortype The selector type diff --git a/tests/dailymail_test.php b/tests/dailymail_test.php index b6df80f5d1..c8675b675c 100644 --- a/tests/dailymail_test.php +++ b/tests/dailymail_test.php @@ -41,11 +41,22 @@ */ class dailymail_test extends \advanced_testcase { + /** @var \stdClass */ private $sink; + + /** @var \stdClass */ private $course; + + /** @var \stdClass */ private $user; + + /** @var \stdClass */ private $moodleoverflow; + + /** @var \stdClass */ private $coursemodule; + + /** @var \stdClass */ private $discussion; /** @@ -81,7 +92,7 @@ public function tearDown(): void { /** * Function that creates a new user, which adds a new discussion an post to the moodleoverflow. - * @param $maildigest The maildigest setting: 0 = off , 1 = on + * @param int $maildigest The maildigest setting: 0 = off , 1 = on */ public function helper_create_user_and_discussion($maildigest) { // Create a user enrolled in the course as student. diff --git a/tests/review_test.php b/tests/review_test.php index b6e2cb0562..29aae0090a 100644 --- a/tests/review_test.php +++ b/tests/review_test.php @@ -30,7 +30,6 @@ global $CFG; require_once($CFG->dirroot . '/mod/moodleoverflow/lib.php'); -require_once($CFG->dirroot . '/mod/moodleoverflow/externallib.php'); /** * PHPUnit Tests for testing readtracking. @@ -38,6 +37,7 @@ * @package mod_moodleoverflow * @copyright 2017 Kennet Winter * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * * @group mod_moodleoverflow */ class review_test extends \advanced_testcase { @@ -105,9 +105,13 @@ protected function tearDown(): void { /** * Test reviews functionality in forums where teachers should review everything. + * + * @runInSeparateProcess */ public function test_forum_review_everything() { - global $DB; + global $DB, $CFG; + require_once($CFG->dirroot . '/mod/moodleoverflow/externallib.php'); + $options = array('course' => $this->course->id, 'needsreview' => review::EVERYTHING, 'forcesubscribe' => MOODLEOVERFLOW_FORCESUBSCRIBE); $moodleoverflow = $this->getDataGenerator()->create_module('moodleoverflow', $options); @@ -180,9 +184,13 @@ public function test_forum_review_everything() { /** * Test reviews functionality in forums where teachers should review questions. + * + * @runInSeparateProcess */ public function test_forum_review_only_questions() { - global $DB; + global $DB, $CFG; + require_once($CFG->dirroot . '/mod/moodleoverflow/externallib.php'); + $options = array('course' => $this->course->id, 'needsreview' => review::QUESTIONS, 'forcesubscribe' => MOODLEOVERFLOW_FORCESUBSCRIBE); $moodleoverflow = $this->getDataGenerator()->create_module('moodleoverflow', $options); diff --git a/tests/userstats_test.php b/tests/userstats_test.php index ca657f7d3c..8c0039bbfa 100644 --- a/tests/userstats_test.php +++ b/tests/userstats_test.php @@ -29,22 +29,53 @@ global $CFG; require_once($CFG->dirroot . '/mod/moodleoverflow/lib.php'); + +/** + * PHPUnit Tests for testing userstats. + * + * @package mod_moodleoverflow + * @copyright 2023 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class userstats_test extends \advanced_testcase { + /** @var \stdClass */ private $course; + + /** @var \stdClass */ private $coursemodule; - private $context; + + /** @var \stdClass */ private $moodleoverflow; + + /** @var \stdClass */ private $teacher; + + /** @var \stdClass */ private $user1; + + /** @var \stdClass */ private $user2; - private $discussion1; // Discussion from user1. - private $discussion2; // Discussion from user2. - private $post1; // First post from discussion1. - private $post2; // First post from discussion2. - private $answer1; // Answerpost to discussion1 from user2. - private $answer2; // Answerpost to discussion2 from user1. - private $generator; // Generator for moodleoverflow. + + /** @var \stdClass */ + private $discussion1; + + /** @var \stdClass */ + private $discussion2; + + /** @var \stdClass */ + private $post1; + + /** @var \stdClass */ + private $post2; + + /** @var \stdClass */ + private $answer1; + + /** @var \stdClass */ + private $answer2; + /** @var \mod_moodleoverflow_generator $generator */ + private $generator; /** * Test setUp. @@ -156,7 +187,6 @@ private function helper_course_set_up() { $location = array('course' => $this->course->id); $this->moodleoverflow = $this->getDataGenerator()->create_module('moodleoverflow', $location); $this->coursemodule = get_coursemodule_from_instance('moodleoverflow', $this->moodleoverflow->id); - $this->context = \context_course::instance($this->course->id); // Create a teacher. $this->teacher = $this->getDataGenerator()->create_user(array('firstname' => 'Tamaro', 'lastname' => 'Walter')); @@ -192,6 +222,12 @@ private function create_statstable() { /** * Create a upvote to a post in an existing discussion. + * + * @param object $author // The creator of the rating. + * @param object $discussion // Discussion object. + * @param object $post // Post that is being rated. + * + * @return $rating */ private function create_upvote($author, $discussion, $post) { $record = (object) [ @@ -208,6 +244,12 @@ private function create_upvote($author, $discussion, $post) { /** * Create a downvote to a post in an existing discussion. + * + * @param object $author // The creator of the rating. + * @param object $discussion // Discussion object. + * @param object $post // Post that is being rated. + * + * @return $rating */ private function create_downvote($author, $discussion, $post) { $record = (object) [ @@ -224,6 +266,12 @@ private function create_downvote($author, $discussion, $post) { /** * Create a helpful rating to a post in an existing discussion. + * + * @param object $author // The creator of the rating. + * @param object $discussion // Discussion object. + * @param object $post // Post that is being rated. + * + * @return $rating */ private function create_helpful($author, $discussion, $post) { $record = (object) [ @@ -240,6 +288,12 @@ private function create_helpful($author, $discussion, $post) { /** * Create a solution rating to a post in an existing discussion. + * + * @param object $author // The creator of the rating. + * @param object $discussion // Discussion object. + * @param object $post // Post that is being rated. + * + * @return $rating */ private function create_solution($author, $discussion, $post) { $record = (object) [ diff --git a/version.php b/version.php index 45384c4fd9..7215a84622 100644 --- a/version.php +++ b/version.php @@ -28,8 +28,8 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_moodleoverflow'; -$plugin->version = 2023050801; -$plugin->release = 'v4.1-r1'; +$plugin->version = 2023050802; +$plugin->release = 'v4.2-r1'; $plugin->requires = 2020061500; // Requires Moodle 3.9+. $plugin->maturity = MATURITY_STABLE; $plugin->dependencies = array(); diff --git a/view.php b/view.php index 52759e13ba..f94467617b 100644 --- a/view.php +++ b/view.php @@ -62,6 +62,10 @@ throw new moodle_exception('missingparameter'); } +// Save the allowmultiplemarks setting. +$marksetting = $DB->get_record('moodleoverflow', array('id' => $moodleoverflow->id), 'allowmultiplemarks'); + + // Require a login. require_login($course, true, $cm); @@ -86,7 +90,7 @@ $PAGE->set_title(format_string($moodleoverflow->name)); $PAGE->set_heading(format_string($course->fullname)); -$PAGE->requires->js_call_amd('mod_moodleoverflow/rating', 'init', [$USER->id]); +$PAGE->requires->js_call_amd('mod_moodleoverflow/rating', 'init', [$USER->id, $marksetting->allowmultiplemarks]); // Output starts here. echo $OUTPUT->header();