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 b/amd/build/rating.min.js index 4dc7b4ea4e..7a148eb2b0 100644 --- a/amd/build/rating.min.js +++ b/amd/build/rating.min.js @@ -1,3 +1,3 @@ -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}}function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof Symbol&&o[Symbol.iterator]||o["@@iterator"];if(!it){if(Array.isArray(o)||(it=function(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(o))||allowArrayLike&&o&&"number"==typeof o.length){it&&(o=it);var i=0,F=function(){};return{s:F,n:function(){return i>=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i.\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 ? '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","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","changeStrings","add","document","getElementById","Ajax","call","methodname","args","ratingid","response","forEach","i","raterreputation","ownerid","ownerreputation","postrating","othermarkedposts"],"mappings":"8kEAsEqBA,OAAQC,sCAChBC,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,uBAAwBhB,uBACjFM,cAAcW,aAAa,4BAA6B,gCAC5B,mBAAU,UAAYR,OAAQ,8BAA1DH,cAAcY,0DAERC,YAAcN,SAAW,WAAa,0BACtCC,SAASF,OAAQC,SAAWO,cAAgBC,gBAAiBrB,uBACnEM,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,iBAC1CiB,aAAef,YAAYgB,UAAUC,SAASJ,WAC9CK,WAAaJ,UAAYK,eAAiBC,cAC1CC,OAASN,aAA4B,GAAbG,WAAkBA,4BAC1Cf,SAASF,OAAQoB,OAAQhC,mBAO1BC,gFAEgBE,KAAK8B,iBAAiB,uBAAyBT,gHAArDU,gBACJP,UAAUQ,OAAOX,6BAEV,iCAAiBf,QAAU,8BADrCyB,GAAGX,qDAA8Cd,cAAY2B,sRAK7DV,4CACAf,YAAYgB,UAAUQ,OAAOX,6BACK,iCAAiBf,QAAU,8BAA7DH,cAAc8B,0BACdC,cAAcb,UAAWf,mBAI5BiB,4CACDf,YAAYgB,UAAUW,IAAId,6BACQ,oCAAoBf,QAAU,8BAAhEH,cAAc8B,0BACVnC,oBACAoC,cAAcb,UAAWf,sOAjH3CY,gBAAkB,EAClBD,cAAgB,EAChBJ,uBAAyB,GACzBD,qBAAuB,GACvBgB,cAAgB,EAChBD,eAAiB,EAEjB3B,KAAOoC,SAASC,eAAe,gCAStB1B,6IAAf,kBAAwBF,OAAQoB,OAAQhC,qJACbyC,cAAKC,KAAK,CAAC,CAC9BC,WAAY,iCACZC,KAAM,CACFhC,OAAQA,OACRiC,SAAUb,WAEd,iBANEc,wBAON3C,KAAK8B,gEAAyDjC,cAAY+C,SAAQ,SAACC,GAC/EA,EAAEZ,YAAcU,SAASG,mBAE7B9C,KAAK8B,gEAAyDa,SAASI,eAAaH,SAAQ,SAACC,GACzFA,EAAEZ,YAAcU,SAASK,mBAE7BhD,KAAK8B,gEAAyDrB,cAAYmC,SAAQ,SAACC,GAC/EA,EAAEZ,YAAcU,SAASM,wCAEtBN,oGAgGIT,8JAAf,kBAA6Bb,UAAWf,+LAC3BP,gBAAgB,qBACrB,CAAC,aAAc,iBAAkB,cAAe,oBAGhDmD,kBAAmB,wCACNlD,KAAK8B,iBAAiB,yIAC5BN,UAAUC,SAASJ,2CACtB6B,kBAAmB,sSAKVlD,KAAK8B,iBAAiB,8HAA5BC,kBACCP,UAAUC,SAASJ,aAAcU,IAAGX,qDAA8Cd,2CAClF4C,oEAEU,qCAAqB5C,QAAU,8BADzCyB,IAAGX,qDAA8Cd,cAAY2B,qFAInD,iCAAiB3B,QAAU,8BADrCyB,IAAGX,qDAA8Cd,cAAY2B"} \ No newline at end of file diff --git a/amd/src/rating.js b/amd/src/rating.js index 7d504d76c7..0f44f3ba77 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']); @@ -104,22 +106,75 @@ export function init(userid) { case 'helpful': case 'solved': { const isHelpful = action === 'helpful'; - const htmlclass = isHelpful ? 'statusstarter' : 'statusteacher'; + const htmlclass = isHelpful ? 'markedhelpful' : 'markedsolution'; const shouldRemove = postElement.classList.contains(htmlclass); 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. + + 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 used 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..7a6f72eb3d 100644 --- a/classes/capabilities.php +++ b/classes/capabilities.php @@ -35,22 +35,57 @@ */ class capabilities { + /** capability add instance */ const ADD_INSTANCE = 'mod/moodleoverflow:addinstance'; + + /** capability view discussions*/ const VIEW_DISCUSSION = 'mod/moodleoverflow:viewdiscussion'; + + /** capability reply in discussions*/ const REPLY_POST = 'mod/moodleoverflow:replypost'; + + /** capability start discussions*/ const START_DISCUSSION = 'mod/moodleoverflow:startdiscussion'; + + /** capability edit post from other course participants*/ const EDIT_ANY_POST = 'mod/moodleoverflow:editanypost'; + + /** capability delete your post*/ const DELETE_OWN_POST = 'mod/moodleoverflow:deleteownpost'; + + /** capability delete post from any course participant*/ const DELETE_ANY_POST = 'mod/moodleoverflow:deleteanypost'; + + /** capability rate a post*/ const RATE_POST = 'mod/moodleoverflow:ratepost'; + + /** capability mark a post as a solution for a questions*/ const MARK_SOLVED = 'mod/moodleoverflow:marksolved'; + + /** capability manage the subscription of a moodleoverflow instance */ const MANAGE_SUBSCRIPTIONS = 'mod/moodleoverflow:managesubscriptions'; + + /** capability force the subscription of participants */ const ALLOW_FORCE_SUBSCRIBE = 'mod/moodleoverflow:allowforcesubscribe'; + + /** capability attach files to posts */ const CREATE_ATTACHMENT = 'mod/moodleoverflow:createattachment'; + + /** capability review post to be published*/ const REVIEW_POST = 'mod/moodleoverflow:reviewpost'; + /** @var array cache capabilities*/ 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/output/moodleoverflow_email.php b/classes/output/moodleoverflow_email.php index fc83e1cf5c..b52d653be9 100644 --- a/classes/output/moodleoverflow_email.php +++ b/classes/output/moodleoverflow_email.php @@ -155,15 +155,15 @@ public function export_for_template(\renderer_base $renderer, $plaintext = false protected function export_for_template_text(\mod_moodleoverflow_renderer $renderer) { return array( - 'id' => html_entity_decode($this->post->id), - 'coursename' => html_entity_decode($this->get_coursename()), - 'courselink' => html_entity_decode($this->get_courselink()), - 'moodleoverflowname' => html_entity_decode($this->get_moodleoverflowname()), - 'showdiscussionname' => html_entity_decode($this->has_showdiscussionname()), - 'discussionname' => html_entity_decode($this->get_discussionname()), - 'subject' => html_entity_decode($this->get_subject()), - 'authorfullname' => html_entity_decode($this->get_author_fullname()), - 'postdate' => html_entity_decode($this->get_postdate()), + 'id' => html_entity_decode($this->post->id, ENT_COMPAT), + 'coursename' => html_entity_decode($this->get_coursename(), ENT_COMPAT), + 'courselink' => html_entity_decode($this->get_courselink(), ENT_COMPAT), + 'moodleoverflowname' => html_entity_decode($this->get_moodleoverflowname(), ENT_COMPAT), + 'showdiscussionname' => html_entity_decode($this->has_showdiscussionname(), ENT_COMPAT), + 'discussionname' => html_entity_decode($this->get_discussionname(), ENT_COMPAT), + 'subject' => html_entity_decode($this->get_subject(), ENT_COMPAT), + 'authorfullname' => html_entity_decode($this->get_author_fullname(), ENT_COMPAT), + 'postdate' => html_entity_decode($this->get_postdate(), ENT_COMPAT), 'firstpost' => $this->is_firstpost(), 'canreply' => $this->canreply, 'permalink' => $this->get_permalink(), @@ -179,7 +179,7 @@ protected function export_for_template_text(\mod_moodleoverflow_renderer $render 'grouppicture' => $this->get_group_picture(), // Format some components according to the renderer. - 'message' => html_entity_decode($renderer->format_message_text($this->cm, $this->post)), + 'message' => html_entity_decode($renderer->format_message_text($this->cm, $this->post), ENT_COMPAT), ); } diff --git a/classes/ratings.php b/classes/ratings.php index 899755d6d1..b9da34156e 100644 --- a/classes/ratings.php +++ b/classes/ratings.php @@ -77,6 +77,10 @@ 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 = (bool) $markssetting->allowmultiplemarks; + // Retrieve the contexts. $modulecontext = \context_module::instance($cm->id); $coursecontext = \context_course::instance($course->id); @@ -138,18 +142,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); } } @@ -213,119 +228,130 @@ public static function moodleoverflow_get_reputation($moodleoverflowid, $userid } /** - * Sort a discussion by the ratings of their posts. + * Sort the answers of a discussion by their marks and votes. * - * @param array $posts - * - * @return array + * @param object $posts all the posts from a discussion. */ public static function moodleoverflow_sort_answers_by_ratings($posts) { - // Create copies to manipulate. - $parentcopy = $posts; - $postscopy = $posts; - $anothercopy = $posts; - - // Check if teacher ratings are prefered. - $preferteacher = (array_shift($anothercopy)->ratingpreference == 1); - - // Create an array with all the keys of the older array. - $oldorder = array(); - foreach ($postscopy as $postid => $post) { - $oldorder[] = $postid; - } - - // Create an array for the new order. - $neworder = array(); - - // The parent post stays the parent post. - $parent = array_shift($parentcopy); - unset($postscopy[$parent->id]); - $discussionid = $parent->discussion; - $neworder[] = (int) $parent->id; - - // Check if answers has been marked. - $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. - if ($statusteacher && $statusstarter) { + // Create a copy that only has the answer posts and save the parent post. + $answerposts = $posts; + $parentpost = array_shift($answerposts); - // Is the same answer correct for both? - if ($statusstarter->postid == $statusteacher->postid) { - - // 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; + // Create an empty array for the sorted posts and add the parent post. + $sortedposts = array(); + $sortedposts[0] = $parentpost; + + // Check if solved posts are preferred over helpful posts. + $solutionspreferred = false; + if ($posts[array_key_first($posts)]->ratingpreference == 1) { + $solutionspreferred = true; + } + + // Sort the answer posts by ratings. + // Build groups of different types of answers (Solved and helpful, only solved/helpful, other). + // markedsolved == 1 means the post is marked as solved. + // markedhelpful == 1 means the post is marked as helpful. + // If a group is complete, sort the group. + $index = 1; + $startsolvedandhelpful = 1; + $startsolved = 1; + $starthelpful = 1; + $startother = 1; + // Solved and helpful posts are first. + foreach ($answerposts as $post) { + if ($post->markedsolution > 0 && $post->markedhelpful > 0) { + $sortedposts[$index] = $post; + $index++; } } - - // If the answers the teacher marks are preferred, and only - // the teacher marked an answer as solved, display it first. - if ($preferteacher && $statusteacher) { - - // 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; - } - - // If the user who started the discussion has marked - // an answer as helpful, display this answer first. - if ($statusstarter) { - - // Add the post to the new order and delete it from the posts array. - $neworder[] = (int) $statusstarter->postid; - unset($postscopy[$statusstarter->postid]); + // Update the indices and sort the group by votes. + if ($index > $startsolvedandhelpful) { + $startsolved = $index; + $starthelpful = $index; + $startother = $index; + self::moodleoverflow_quicksort_post_by_votes($sortedposts, $startsolvedandhelpful, $index - 1); } - // If a teacher has marked an answer as solved, display it next. - if ($statusteacher) { + // Check if solutions are preferred. + if ($solutionspreferred) { - // Add the post to the new order and delete it from the posts array. - $neworder[] = (int) $statusteacher->postid; - unset($postscopy[$statusteacher->postid]); - } + // Build the group of only solved posts. + foreach ($answerposts as $post) { + if ($post->markedsolution > 0 && $post->markedhelpful == 0) { + $sortedposts[$index] = $post; + $index++; + } + } + // Update the indices and sort the group by votes. + if ($index > $startsolved) { + $starthelpful = $index; + $startother = $index; + self::moodleoverflow_quicksort_post_by_votes($sortedposts, $startsolved, $index - 1); + } - // All answers that are not marked by someone should now be left. + // Build the group of only helpful posts. + foreach ($answerposts as $post) { + if ($post->markedsolution == 0 && $post->markedhelpful > 0) { + $sortedposts[$index] = $post; + $index++; + } + } + // Update the indices and sort the group by votes. + if ($index > $starthelpful) { + $startother = $index; + self::moodleoverflow_quicksort_post_by_votes($sortedposts, $starthelpful, $index - 1); + } + } else { - // Search for all comments. - foreach ($postscopy as $postid => $post) { + // Build the group of only helpful posts. + foreach ($answerposts as $post) { + if ($post->markedsolution == 0 && $post->markedhelpful > 0) { + $sortedposts[$index] = $post; + $index++; + } + } + // Update the indices and sort the group by votes. + if ($index > $starthelpful) { + $startsolved = $index; + $startother = $index; + self::moodleoverflow_quicksort_post_by_votes($sortedposts, $starthelpful, $index - 1); + } - // Add all comments to the order. - // They are independant from the votes. - if ($post->parent != $parent->id) { - $neworder[] = $postid; - unset($postscopy[$postid]); + // Build the group of only solved posts. + foreach ($answerposts as $post) { + if ($post->markedsolution > 0 && $post->markedhelpful == 0) { + $sortedposts[$index] = $post; + $index++; + } + } + // Update the indices and sort the group by votes. + if ($index > $startsolved) { + $startother = $index; + self::moodleoverflow_quicksort_post_by_votes($sortedposts, $startsolved, $index - 1); } } - // Sort the remaining answers by their total votes. - $votesarray = array(); - foreach ($postscopy as $postid => $post) { - $votesarray[$post->id] = $post->upvotes - $post->downvotes; + // Now build the group of posts without ratings like helpful/solved. + foreach ($answerposts as $post) { + if ($post->markedsolution == 0 && $post->markedhelpful == 0) { + $sortedposts[$index] = $post; + $index++; + } } - arsort($votesarray); - - // Add the remaining messages to the new order. - foreach ($votesarray as $postid => $votes) { - $neworder[] = $postid; + // Update the indices and sort the group by votes. + if ($index > $startother) { + self::moodleoverflow_quicksort_post_by_votes($sortedposts, $startother, $index - 1); } - // The new order is determined. - // It has to be applied now. - $sortedposts = array(); - foreach ($neworder as $k) { - $sortedposts[$k] = $posts[$k]; + // Rearrange the indices and return the sorted posts. + + $neworder = array(); + foreach ($sortedposts as $post) { + $neworder[$post->id] = $post; } - // Return the sorted posts. - return $sortedposts; + // Return now the sorted posts. + return $neworder; } /** @@ -426,8 +452,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 +463,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 +813,40 @@ public static function moodleoverflow_user_can_rate($post, $modulecontext, $user && $post->reviewed == 1; } + /** + * Sorts answerposts of a discussion with quicksort algorithm + * @param array $posts the posts that are being sorted + * @param int $low the index from where the sorting begins + * @param int $high the index until the array is being sorted + */ + private static function moodleoverflow_quicksort_post_by_votes(array &$posts, $low, $high) { + if ($low >= $high) { + return; + } + $left = $low; + $right = $high; + $pivot = $posts[intval(($low + $high) / 2)]->votesdifference; + do { + while ($posts[$left]->votesdifference > $pivot) { + $left++; + } + while ($posts[$right]->votesdifference < $pivot) { + $right--; + } + if ($left <= $right) { + $temp = $posts[$right]; + $posts[$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_daily_mail.php b/classes/task/send_daily_mail.php index e2c57a57e2..7c2fa1cda9 100644 --- a/classes/task/send_daily_mail.php +++ b/classes/task/send_daily_mail.php @@ -23,7 +23,6 @@ */ namespace mod_moodleoverflow\task; -defined('MOODLE_INTERNAL') || die(); /** * This task sends a daily mail of unread posts */ 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..ef1ec7a7b9 100644 --- a/locallib.php +++ b/locallib.php @@ -265,27 +265,31 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = - } // Check if the question owner marked the question as helpful. - $statusstarter = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->discussion, false); + $markedhelpful = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->discussion, false); $preparedarray[$i]['starterlink'] = null; - if ($statusstarter) { + if ($markedhelpful) { $link = '/mod/moodleoverflow/discussion.php?d='; + $markedhelpful = $markedhelpful[array_key_first($markedhelpful)]; + $preparedarray[$i]['starterlink'] = new moodle_url($link . - $statusstarter->discussionid . '#p' . $statusstarter->postid); + $markedhelpful->discussionid . '#p' . $markedhelpful->postid); } // Check if a teacher marked a post as solved. - $statusteacher = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->discussion, true); + $markedsolution = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->discussion, true); $preparedarray[$i]['teacherlink'] = null; - if ($statusteacher) { + if ($markedsolution) { $link = '/mod/moodleoverflow/discussion.php?d='; + $markedsolution = $markedsolution[array_key_first($markedsolution)]; + $preparedarray[$i]['teacherlink'] = new moodle_url($link . - $statusteacher->discussionid . '#p' . $statusteacher->postid); + $markedsolution->discussionid . '#p' . $markedsolution->postid); } // Check if a single post was marked by the question owner and a teacher. $statusboth = false; - if ($statusstarter && $statusteacher) { - if ($statusstarter->postid == $statusteacher->postid) { + if ($markedhelpful && $markedsolution) { + if ($markedhelpful->postid == $markedsolution->postid) { $statusboth = true; } } @@ -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; @@ -396,8 +401,8 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = - $preparedarray[$i]['linktopopup'] = $linktopopup; // Add all created data to an array. - $preparedarray[$i]['statusstarter'] = $statusstarter; - $preparedarray[$i]['statusteacher'] = $statusteacher; + $preparedarray[$i]['markedhelpful'] = $markedhelpful; + $preparedarray[$i]['markedsolution'] = $markedsolution; $preparedarray[$i]['statusboth'] = $statusboth; $preparedarray[$i]['votes'] = $votes; @@ -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 multiplemarks (default: multiplemarks are not allowed) */ -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,8 +1059,9 @@ 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]->statusstarter = $discussionratings[$post->id]->ishelpful; - $posts[$postid]->statusteacher = $discussionratings[$post->id]->issolved; + $posts[$postid]->votesdifference = $posts[$postid]->upvotes - $posts[$postid]->downvotes; + $posts[$postid]->markedhelpful = $discussionratings[$post->id]->ishelpful; + $posts[$postid]->markedsolution = $discussionratings[$post->id]->issolved; } // Order the answers by their ratings. @@ -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 setting of multiplemarks * @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,34 +1228,54 @@ 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) { + if ($post->markedhelpful) { $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. $link = '/mod/moodleoverflow/discussion.php'; - if ($post->statusteacher) { + if ($post->markedsolution) { $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,15 +1330,15 @@ 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. $mustachedata->votes = $post->upvotes - $post->downvotes; // Check if the post is marked. - $mustachedata->statusstarter = $post->statusstarter; - $mustachedata->statusteacher = $post->statusteacher; + $mustachedata->markedhelpful = $post->markedhelpful; + $mustachedata->markedsolution = $post->markedsolution; // Did the user rated this post? $rating = \mod_moodleoverflow\ratings::moodleoverflow_user_rated($post->id); @@ -1345,11 +1379,11 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co } } } - if ($post->statusstarter) { - $postclass .= ' statusstarter'; + if ($post->markedhelpful) { + $postclass .= ' markedhelpful'; } - if ($post->statusteacher) { - $postclass .= ' statusteacher'; + if ($post->markedsolution) { + $postclass .= ' markedsolution'; } $mustachedata->postclass = $postclass; @@ -1462,13 +1496,14 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co * @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 * @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/styles.css b/styles.css index c6c3c8fbf7..f0c92ff5bf 100644 --- a/styles.css +++ b/styles.css @@ -191,7 +191,7 @@ width: 4px; } -.moodleoverflowpost.statusteacher.statusstarter:before { +.moodleoverflowpost.markedsolution.markedhelpful:before { background: linear-gradient(80deg, rgba(234, 133, 22, 1) 50%, rgba(112, 160, 52, 1) 50%); bottom: -1px; content: ""; @@ -202,7 +202,7 @@ width: 4px; } -.moodleoverflowpost.statusteacher:before { +.moodleoverflowpost.markedsolution:before { background: rgba(112, 160, 52, 1); bottom: -1px; content: ""; @@ -213,7 +213,7 @@ width: 4px; } -.moodleoverflowpost.statusstarter:before { +.moodleoverflowpost.markedhelpful:before { background: rgba(234, 133, 22, 1); bottom: -1px; content: ""; @@ -229,11 +229,11 @@ display: none; } -.moodleoverflowpost.statusstarter .onlyifhelpful { +.moodleoverflowpost.markedhelpful .onlyifhelpful { display: initial; } -.moodleoverflowpost.statusteacher .onlyifsolved { +.moodleoverflowpost.markedsolution .onlyifsolved { display: initial; } diff --git a/templates/discussion_list.mustache b/templates/discussion_list.mustache index 19902f0a78..1cb4a05c76 100644 --- a/templates/discussion_list.mustache +++ b/templates/discussion_list.mustache @@ -119,23 +119,23 @@ {{#pix}}i/duration, moodle, {{#str}}pending_review, mod_moodleoverflow{{/str}}{{/pix}} {{/ questionunderreview }} {{^ questionunderreview }} - {{#statusteacher}} + {{#markedsolution}} {{# pix}} status/c_outline, moodleoverflow, {{#str}}teacherrating, moodleoverflow{{/str}} {{/ pix}} - {{/statusteacher}} - {{^statusteacher}} + {{/markedsolution}} + {{^markedsolution}} {{# pix}} status/c_blank, moodleoverflow, {{#str}}marknotsolved, moodleoverflow{{/str}}{{/ pix}} - {{/statusteacher}} + {{/markedsolution}} - {{#statusstarter}} + {{#markedhelpful}} {{# pix}} status/b_outline, moodleoverflow, {{#str}}starterrating, moodleoverflow{{/str}} {{/ pix}} - {{/statusstarter}} - {{^statusstarter}} + {{/markedhelpful}} + {{^markedhelpful}} {{# pix}} status/b_blank, moodleoverflow, {{#str}}marknothelpful, moodleoverflow{{/str}} {{/ pix}} - {{/statusstarter}} + {{/markedhelpful}} {{/ questionunderreview }} diff --git a/templates/discussions.mustache b/templates/discussions.mustache index e8c30202ea..48340589d3 100644 --- a/templates/discussions.mustache +++ b/templates/discussions.mustache @@ -46,16 +46,16 @@ {{#pix}}i/duration, moodle, {{#str}}pending_review, mod_moodleoverflow{{/str}}{{/pix}} {{/ questionunderreview }} {{^ questionunderreview }} - {{#statusteacher}} + {{#markedsolution}} {{! avoid whitespace !}}{{# pix}} i/status-solved, moodleoverflow, {{#str}}containsteacherrating, moodleoverflow{{/str}} {{/ pix}}{{! !}} - {{/statusteacher}} - {{#statusstarter}} + {{/markedsolution}} + {{#markedhelpful}} {{! avoid whitespace !}}{{# pix}} i/status-helpful, moodleoverflow, {{#str}}containsstarterrating, moodleoverflow{{/str}} {{/ pix}}{{! !}} - {{/ statusstarter }} + {{/ markedhelpful }} {{/ questionunderreview }}
diff --git a/tests/behat/behat_mod_moodleoverflow.php b/tests/behat/behat_mod_moodleoverflow.php index 7d1375c6c1..709dae2ef6 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()="'. diff --git a/tests/dailymail_test.php b/tests/dailymail_test.php index b6df80f5d1..26447424b1 100644 --- a/tests/dailymail_test.php +++ b/tests/dailymail_test.php @@ -41,11 +41,22 @@ */ class dailymail_test extends \advanced_testcase { + /** @var \stdClass collection of messages */ private $sink; + + /** @var \stdClass test course */ private $course; + + /** @var \stdClass test user*/ private $user; + + /** @var \stdClass moodleoverflow instance */ private $moodleoverflow; + + /** @var \stdClass coursemodule instance */ private $coursemodule; + + /** @var \stdClass discussion instance */ private $discussion; /** @@ -75,13 +86,11 @@ public function tearDown(): void { \mod_moodleoverflow\subscriptions::reset_discussion_cache(); } - - // Helper functions. /** * 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. @@ -125,6 +134,7 @@ private function helper_run_send_mails() { /** * Test if the task send_daily_mail sends a mail to the user. + * @covers \send_daily_mail::execute */ public function test_mail_delivery() { @@ -142,6 +152,7 @@ public function test_mail_delivery() { /** * Test if the content of the mail matches the supposed content. + * @covers \send_daily_mail::execute */ public function test_content_of_mail_delivery() { @@ -176,6 +187,7 @@ public function test_content_of_mail_delivery() { /** * Test if the task does not send a mail when maildigest = 0 + * @covers \send_daily_mail::execute */ public function test_mail_not_send() { // Creat user with daily_mail = off. @@ -191,6 +203,7 @@ public function test_mail_not_send() { /** * Test if database is updated after sending a mail + * @covers \send_daily_mail::execute */ public function test_records_removed() { global $DB; diff --git a/tests/ratings_test.php b/tests/ratings_test.php new file mode 100644 index 0000000000..c7fb701f6e --- /dev/null +++ b/tests/ratings_test.php @@ -0,0 +1,654 @@ +. + +/** + * The module moodleoverflow tests. + * + * @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\ratings; + +defined('MOODLE_INTERNAL') || die(); + + +global $CFG; +require_once($CFG->dirroot . '/mod/moodleoverflow/locallib.php'); + +/** + * PHPUnit Tests for testing the ratings.php. + * + * @package mod_moodleoverflow + * @copyright 2023 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +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; + + /** @var \stdClass answer from user 1 */ + private $answer1; + + /** @var \stdClass answer from user 1 */ + private $answer2; + + /** @var \stdClass answer from user 1 */ + private $answer3; + + /** @var \stdClass answer from user 2 */ + private $answer4; + + /** @var \stdClass answer from user 2 */ + private $answer5; + + /** @var \stdClass answer from user 2 */ + private $answer6; + + /** @var \mod_moodleoverflow_generator $generator */ + private $generator; + + /** + * Test setUp. + */ + public function setUp(): void { + $this->resetAfterTest(); + $this->helper_course_set_up(); + } + + /** + * Test tearDown. + */ + public function tearDown(): void { + // Clear all caches. + \mod_moodleoverflow\subscriptions::reset_moodleoverflow_cache(); + \mod_moodleoverflow\subscriptions::reset_discussion_cache(); + } + + // Begin of test functions. + + /** + * Tests function ratings::moodleoverflow_sort_answer_by_ratings + * Test case: Every group of rating exists (helful and solved posts, only helpful/solved and none) + * @covers \ratings::moodleoverflow_sort_answer_by_ratings() + */ + public function test_answersorting_everygroup() { + // Create helpful, solved, up and downvotes ratings. + $this->create_everygroup(); + + // Test with every group of rating. + + // Create a array of the posts, save the sorted post and compare them to the order that they should have. + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $this->set_ratingpreferences(0); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer1, $this->answer3, $this->answer2, + $this->answer4, $this->answer6, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Change the rating preference of the teacher and sort again. + $this->set_ratingpreferences(1); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer1, $this->answer2, $this->answer3, + $this->answer4, $this->answer6, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + } + + /** + * Tests function ratings::moodleoverflow_sort_answer_by_ratings + * Test case: One group of rating does not exist + * @covers \ratings::moodleoverflow_sort_answer_by_ratings() + */ + public function test_answersorting_threegroups() { + // Create helpful, solved, up and downvotes ratings. + $this->create_everygroup(); + + // Test without posts that are only marked as solved. + $posts = array($this->post, $this->answer1, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $this->set_ratingpreferences(0); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer1, $this->answer3, $this->answer4, $this->answer6, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + $this->set_ratingpreferences(1); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer1, $this->answer3, $this->answer4, $this->answer6, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Test without posts that are only marked as helpful. + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer4, $this->answer5, $this->answer6); + $this->set_ratingpreferences(0); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer1, $this->answer2, $this->answer4, $this->answer6, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + $this->set_ratingpreferences(1); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer1, $this->answer2, $this->answer4, $this->answer6, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Test without posts that are marked as both helpful and solved. + $posts = array($this->post, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $this->set_ratingpreferences(0); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer3, $this->answer2, $this->answer4, $this->answer6, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + $this->set_ratingpreferences(1); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer2, $this->answer3, $this->answer4, $this->answer6, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + } + + /** + * Tests function ratings::moodleoverflow_sort_answer_by_ratings + * Test case: two groups of rating do not exist + * @covers \ratings::moodleoverflow_sort_answer_by_ratings() + */ + public function test_answersorting_twogroups() { + $this->set_ratingpreferences(0); + + // Test case 1: helpful and solved post, only solved posts. + $group1 = 'sh'; + $group2 = 's'; + $this->create_twogroups($group1, $group2); + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer2, $this->answer1, $this->answer3, + $this->answer6, $this->answer5, $this->answer4); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Test case 2: helpful and solved post, only helpful posts. + $group1 = 'sh'; + $group2 = 'h'; + $this->create_twogroups($group1, $group2); + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer2, $this->answer1, $this->answer3, + $this->answer6, $this->answer5, $this->answer4); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Test case 3: helpful and solved post, not-marked posts. + $group1 = 'sh'; + $group2 = 'o'; + $this->create_twogroups($group1, $group2); + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer2, $this->answer1, $this->answer3, + $this->answer6, $this->answer5, $this->answer4); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Test case 4: only solved posts and only helpful posts with ratingpreferences = 0. + $group1 = 's'; + $group2 = 'h'; + $this->set_ratingpreferences(0); + $this->create_twogroups($group1, $group2); + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer6, $this->answer5, $this->answer4, + $this->answer2, $this->answer1, $this->answer3); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Test case 5: only solved posts and only helpful posts with ratingpreferences = 1. + $group1 = 's'; + $group2 = 'h'; + $this->set_ratingpreferences(1); + $this->create_twogroups($group1, $group2); + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer2, $this->answer1, $this->answer3, + $this->answer6, $this->answer5, $this->answer4); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Test case 6: only solved posts and not-marked posts. + $group1 = 's'; + $group2 = 'o'; + $this->create_twogroups($group1, $group2); + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer2, $this->answer1, $this->answer3, + $this->answer6, $this->answer5, $this->answer4); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Test case 6: only helpful posts and not-marked posts. + $group1 = 'h'; + $group2 = 'o'; + $this->create_twogroups($group1, $group2); + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer2, $this->answer1, $this->answer3, + $this->answer6, $this->answer5, $this->answer4); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + } + + /** + * Tests function ratings::moodleoverflow_sort_answer_by_ratings + * Test case: Only one group of rating exists, so only: + * - helpful and solved posts, or + * - helpful, or + * - solved, or + * - not marked + * @covers \ratings::moodleoverflow_sort_answer_by_ratings() + */ + public function test_answersorting_onegroup() { + $this->set_ratingpreferences(0); + + // Test case 1: only solved and helpful posts. + $group = 'sh'; + $this->create_onegroup($group); + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer4, $this->answer6, $this->answer3, + $this->answer1, $this->answer2, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Test case 1: only solvedposts. + $group = 's'; + $this->create_onegroup($group); + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer4, $this->answer6, $this->answer3, + $this->answer1, $this->answer2, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Test case 1: only helpful posts. + $group = 'h'; + $this->create_onegroup($group); + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer4, $this->answer6, $this->answer3, + $this->answer1, $this->answer2, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + + // Test case 1: only not marked posts. + $group = 'o'; + $this->create_onegroup($group); + $posts = array($this->post, $this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + $sortedposts = ratings::moodleoverflow_sort_answers_by_ratings($posts); + $rightorderposts = array($this->post, $this->answer4, $this->answer6, $this->answer3, + $this->answer1, $this->answer2, $this->answer5); + $result = $this->postsorderequal($sortedposts, $rightorderposts); + $this->assertEquals(1, $result); + } + + // Helper functions. + + /** + * This function creates: + * - a course with a moodleoverflow + * - a teacher, who creates a discussion with a post + * - 2 users, which answer to the post from the teacher + */ + private function helper_course_set_up() { + global $DB; + // Create a new course with a moodleoverflow forum. + $this->course = $this->getDataGenerator()->create_course(); + $location = array('course' => $this->course->id); + $this->moodleoverflow = $this->getDataGenerator()->create_module('moodleoverflow', $location); + $this->coursemodule = get_coursemodule_from_instance('moodleoverflow', $this->moodleoverflow->id); + + // Create a teacher. + $this->teacher = $this->getDataGenerator()->create_user(array('firstname' => 'Tamaro', 'lastname' => 'Walter')); + $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, 'student'); + + // Create 2 users. + $this->user1 = $this->getDataGenerator()->create_user(array('firstname' => 'Ava', 'lastname' => 'Davis')); + $this->getDataGenerator()->enrol_user($this->user1->id, $this->course->id, 'student'); + $this->user2 = $this->getDataGenerator()->create_user(array('firstname' => 'Ethan', 'lastname' => 'Brown')); + $this->getDataGenerator()->enrol_user($this->user2->id, $this->course->id, 'student'); + + // Create a discussion, a parent post and six answers. + $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', array('id' => $this->discussion[0]->firstpost), '*'); + $this->answer1 = $this->generator->reply_to_post($this->discussion[1], $this->user1, true); + $this->answer2 = $this->generator->reply_to_post($this->discussion[1], $this->user1, true); + $this->answer3 = $this->generator->reply_to_post($this->discussion[1], $this->user1, true); + $this->answer4 = $this->generator->reply_to_post($this->discussion[1], $this->user2, true); + $this->answer5 = $this->generator->reply_to_post($this->discussion[1], $this->user2, true); + $this->answer6 = $this->generator->reply_to_post($this->discussion[1], $this->user2, true); + } + + + /** + * This function compares 2 arrays with posts and checks if the order is the same. + * + * @param array $sortedposts - The sorted posts + * @param array $rightorderposts - The posts with the order they should have + * The function returns 1 if $sortedposts matches $posts, else 0 + */ + private function postsorderequal($sortedposts, $rightorderposts) { + if (count($sortedposts) != count($rightorderposts)) { + return 0; + } + for ($i = 0; $i < count($sortedposts); $i++) { + // Get the current elements. + $sortedpost = current($sortedposts); + $post = current($rightorderposts); + if ($sortedpost->id == $post->id) { + // Go to the next elements. + next($sortedposts); + next($rightorderposts); + } else { + return 0; + } + } + return 1; + } + + /** + * sets the ratingpreferences to 1 or 0: + * 1 = solved posts will be shown above helpful posts. + * 0 = helpful posts will be shown above solved posts. + * @param int $preference the rating preference + */ + private function set_ratingpreferences($preference) { + if ($preference == 0 || $preference == 1) { + $this->post->ratingpreference = $preference; + $this->answer1->ratingpreference = $preference; + $this->answer2->ratingpreference = $preference; + $this->answer3->ratingpreference = $preference; + $this->answer4->ratingpreference = $preference; + $this->answer5->ratingpreference = $preference; + $this->answer6->ratingpreference = $preference; + } + } + + // Creation functions, that create different rating situations of the posts in a discussion. + + /** + * creates a rating of every type by adding attributes to the post: + * - post that is solved and helpful + * . post that is only helpful + * - post that is only solved + * - post that is not marked + */ + private function create_everygroup() { + // Answer1. + $this->answer1->upvotes = 0; + $this->answer1->downvotes = 0; + $this->answer1->votesdifference = $this->answer1->upvotes - $this->answer1->downvotes; + $this->answer1->markedhelpful = 1; + $this->answer1->markedsolution = 1; + + // Answer2. + $this->answer2->upvotes = 0; + $this->answer2->downvotes = 0; + $this->answer2->votesdifference = $this->answer2->upvotes - $this->answer2->downvotes; + $this->answer2->markedhelpful = 0; + $this->answer2->markedsolution = 1; + + // Answer3. + $this->answer3->upvotes = 0; + $this->answer3->downvotes = 0; + $this->answer3->votesdifference = $this->answer3->upvotes - $this->answer3->downvotes; + $this->answer3->markedhelpful = 1; + $this->answer3->markedsolution = 0; + + // Answer4. + $this->answer4->upvotes = 1; + $this->answer4->downvotes = 0; + $this->answer4->votesdifference = $this->answer4->upvotes - $this->answer4->downvotes; + $this->answer4->markedhelpful = 0; + $this->answer4->markedsolution = 0; + + // Answer5. + $this->answer5->upvotes = 0; + $this->answer5->downvotes = 1; + $this->answer5->votesdifference = $this->answer5->upvotes - $this->answer5->downvotes; + $this->answer5->markedhelpful = 0; + $this->answer5->markedsolution = 0; + + // Answer6. + $this->answer6->upvotes = 0; + $this->answer6->downvotes = 0; + $this->answer6->votesdifference = $this->answer6->upvotes - $this->answer6->downvotes; + $this->answer6->markedhelpful = 0; + $this->answer6->markedsolution = 0; + } + + /** + * Creates a rating of one group for every post in the discussion + * Creates up and downvotes + * @param string $group + * A Group can be: + * - both as solution and helpful marked posts (sh) + * - only solution posts (s) + * - only helpful (h) + * - no mark (o) + */ + private function create_onegroup($group) { + $answers = array($this->answer1, $this->answer2, $this->answer3, $this->answer4, $this->answer5, $this->answer6); + foreach ($answers as $answer) { + switch ($group) { + case 'sh': + $answer->markedhelpful = 1; + $answer->markedsolution = 1; + break; + case 's': + $answer->markedhelpful = 0; + $answer->markedsolution = 1; + break; + case 'h': + $answer->markedhelpful = 1; + $answer->markedsolution = 0; + break; + case 'o': + $answer->markedhelpful = 0; + $answer->markedsolution = 0; + break; + } + } + + // Votes for the answerposts + // Answer1. + $this->answer1->upvotes = 4; + $this->answer1->downvotes = 4; + $this->answer1->votesdifference = $this->answer1->upvotes - $this->answer1->downvotes; // Vd = 0. + + // Answer2. + $this->answer2->upvotes = 1; + $this->answer2->downvotes = 2; + $this->answer2->votesdifference = $this->answer2->upvotes - $this->answer2->downvotes; // Vd = -1. + + // Answer3. + $this->answer3->upvotes = 3; + $this->answer3->downvotes = 2; + $this->answer3->votesdifference = $this->answer3->upvotes - $this->answer3->downvotes; // Vd = 1. + + // Answer4. + $this->answer4->upvotes = 5; + $this->answer4->downvotes = 0; + $this->answer4->votesdifference = $this->answer4->upvotes - $this->answer4->downvotes; // Vd = 5. + + // Answer5. + $this->answer5->upvotes = 0; + $this->answer5->downvotes = 2; + $this->answer5->votesdifference = $this->answer5->upvotes - $this->answer5->downvotes; // Vd = -2. + + // Answer6. + $this->answer6->upvotes = 4; + $this->answer6->downvotes = 2; + $this->answer6->votesdifference = $this->answer6->upvotes - $this->answer6->downvotes; // Vd = 2. + + // Rightorder = answer4 , answer6, answer3, answer1, answer2, answer5. + } + + /** + * Creates ratings of the posts of the assigned groups in the discussion. + * Creates up and downvotes + * @param string $group1 + * @param string $group2 + * A Group can be: + * - both as solution and helpful marked posts (sh) + * - only solution posts (s) + * - only helpful (h) + * - no mark (o) + */ + private function create_twogroups($group1, $group2) { + // Set the first 3 answers to the first group of rating. + switch ($group1) { + case 'sh': + $this->answer1->markedhelpful = 1; + $this->answer1->markedsolution = 1; + $this->answer2->markedhelpful = 1; + $this->answer2->markedsolution = 1; + $this->answer3->markedhelpful = 1; + $this->answer3->markedsolution = 1; + break; + case 's': + $this->answer1->markedhelpful = 0; + $this->answer1->markedsolution = 1; + $this->answer2->markedhelpful = 0; + $this->answer2->markedsolution = 1; + $this->answer3->markedhelpful = 0; + $this->answer3->markedsolution = 1; + break; + case 'h': + $this->answer1->markedhelpful = 1; + $this->answer1->markedsolution = 0; + $this->answer2->markedhelpful = 1; + $this->answer2->markedsolution = 0; + $this->answer3->markedhelpful = 1; + $this->answer3->markedsolution = 0; + break; + case 'o': + $this->answer1->markedhelpful = 0; + $this->answer1->markedsolution = 0; + $this->answer2->markedhelpful = 0; + $this->answer2->markedsolution = 0; + $this->answer3->markedhelpful = 0; + $this->answer3->markedsolution = 0; + break; + } + + switch ($group2) { + case 'sh': + $this->answer4->markedhelpful = 1; + $this->answer4->markedsolution = 1; + $this->answer5->markedhelpful = 1; + $this->answer5->markedsolution = 1; + $this->answer6->markedhelpful = 1; + $this->answer6->markedsolution = 1; + break; + case 's': + $this->answer4->markedhelpful = 0; + $this->answer4->markedsolution = 1; + $this->answer5->markedhelpful = 0; + $this->answer5->markedsolution = 1; + $this->answer6->markedhelpful = 0; + $this->answer6->markedsolution = 1; + break; + case 'h': + $this->answer4->markedhelpful = 1; + $this->answer4->markedsolution = 0; + $this->answer5->markedhelpful = 1; + $this->answer5->markedsolution = 0; + $this->answer6->markedhelpful = 1; + $this->answer6->markedsolution = 0; + break; + case 'o': + $this->answer4->markedhelpful = 0; + $this->answer4->markedsolution = 0; + $this->answer5->markedhelpful = 0; + $this->answer5->markedsolution = 0; + $this->answer6->markedhelpful = 0; + $this->answer6->markedsolution = 0; + break; + } + + // Now set the up and downvotes for every answer. + // Answer1. + $this->answer1->upvotes = 3; + $this->answer1->downvotes = 4; + $this->answer1->votesdifference = $this->answer1->upvotes - $this->answer1->downvotes; // Vd = -1. + + // Answer2. + $this->answer2->upvotes = 4; + $this->answer2->downvotes = 1; + $this->answer2->votesdifference = $this->answer2->upvotes - $this->answer2->downvotes; // Vd = 3. + + // Answer3. + $this->answer3->upvotes = 0; + $this->answer3->downvotes = 2; + $this->answer3->votesdifference = $this->answer3->upvotes - $this->answer3->downvotes; // Vd = -2. + + // Answer4. + $this->answer4->upvotes = 5; + $this->answer4->downvotes = 5; + $this->answer4->votesdifference = $this->answer4->upvotes - $this->answer4->downvotes; // Vd = 0. + + // Answer5. + $this->answer5->upvotes = 6; + $this->answer5->downvotes = 5; + $this->answer5->votesdifference = $this->answer5->upvotes - $this->answer5->downvotes; // Vd = 1. + + // Answer6. + $this->answer6->upvotes = 4; + $this->answer6->downvotes = 2; + $this->answer6->votesdifference = $this->answer6->upvotes - $this->answer6->downvotes; // Vd = 2. + + // The Rightorder depends now on the group parameter. + // Rightorder (sh,s) = answer2, answer1, answer3, answer6, answer5, answer4. + // Rightorder (sh,h) = answer2, answer1, answer3, answer6, answer5, answer4. + // Rightorder (sh,o) = answer2, answer1, answer3, answer6, answer5, answer4. + // Rightorder (s,h) = answer6, answer5, answer4, answer2, answer1, answer3. with ratingpreference = 0. + // Rightorder (s,h) = answer2, answer1, answer3, answer6, answer5, answer4. with ratingpreference = 1 + // Rightorder (s,o) = answer2, answer1, answer3, answer6, answer5, answer4. + // Rightorder (h,o) = answer2, answer1, answer3, answer6, answer5, answer4. + + } +} 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..9b26db5e56 100644 --- a/tests/userstats_test.php +++ b/tests/userstats_test.php @@ -29,22 +29,54 @@ 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 test course */ private $course; + + /** @var \stdClass coursemodule */ private $coursemodule; - private $context; + + /** @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; - 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 a discussion */ + private $discussion1; + + /** @var \stdClass another faked discussion */ + private $discussion2; + + /** @var \stdClass a post */ + private $post1; + + /** @var \stdClass another post */ + private $post2; + + /** @var \stdClass answer to a post */ + private $answer1; + + /** @var \stdClass another answer to a post */ + private $answer2; + + /** @var \mod_moodleoverflow_generator $generator */ + private $generator; /** * Test setUp. @@ -67,6 +99,7 @@ public function tearDown(): void { /** * Test, if a upvote is being counted. + * @covers \userstats_table */ public function test_upvote() { // Teacher upvotes the discussion and the answer of user2. @@ -84,6 +117,7 @@ public function test_upvote() { /** * Test, if a downvote is being counted. + * @covers \userstats_table */ public function test_downvote() { // Teacher downvotes the discussion and the answer of user1. @@ -101,6 +135,7 @@ public function test_downvote() { /** * Test, if the activity is calculated correctly. + * @covers \userstats_table */ public function test_activity() { // User1 will rates 3 times. @@ -121,6 +156,7 @@ public function test_activity() { } /** * Test, if the reputation is calculated correctly. + * @covers \userstats_table */ public function test_reputation() { // User1 creates some ratings for user2, Teacher creates some ratings for user2. @@ -156,7 +192,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 +227,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 +249,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 +271,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 +293,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..b70eabf999 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 = 2023052200; +$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();