diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..dfe57bd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false +insert_final_newline = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml new file mode 100644 index 0000000..50f9d58 --- /dev/null +++ b/.github/workflows/moodle-ci.yml @@ -0,0 +1,130 @@ +name: build + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-18.04 + + services: + postgres: + image: postgres:9.6 + env: + POSTGRES_USER: 'postgres' + POSTGRES_HOST_AUTH_METHOD: 'trust' + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 + mariadb: + image: mariadb:10 + env: + MYSQL_USER: 'root' + MYSQL_ALLOW_EMPTY_PASSWORD: "true" + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3 + + strategy: + fail-fast: false + matrix: + php: ['7.2', '7.3', '7.4'] + moodle-branch: ['MOODLE_310_STABLE', 'MOODLE_39_STABLE'] + database: [pgsql, mariadb] + + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + path: plugin + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Initialise moodle-plugin-ci + run: | + composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3 + echo $(cd ci/bin; pwd) >> $GITHUB_PATH + echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH + sudo locale-gen en_AU.UTF-8 + + - name: Install moodle-plugin-ci + run: | + moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1 + env: + DB: ${{ matrix.database }} + MOODLE_BRANCH: ${{ matrix.moodle-branch }} + + - name: PHP Lint + if: ${{ always() }} + run: moodle-plugin-ci phplint + + - name: PHP Copy/Paste Detector + continue-on-error: true # This step will show errors but will not fail + if: ${{ always() }} + run: moodle-plugin-ci phpcpd + + - name: PHP Mess Detector + continue-on-error: true # This step will show errors but will not fail + if: ${{ always() }} + run: moodle-plugin-ci phpmd + + - name: Moodle Code Checker + if: ${{ always() }} + run: moodle-plugin-ci codechecker --max-warnings 0 + + - name: Moodle PHPDoc Checker + if: ${{ always() }} + run: moodle-plugin-ci phpdoc + + - name: Validating + if: ${{ always() }} + run: moodle-plugin-ci validate + + - name: Check upgrade savepoints + if: ${{ always() }} + run: moodle-plugin-ci savepoints + + - name: Mustache Lint + if: ${{ always() }} + run: moodle-plugin-ci mustache + + - name: Grunt + if: ${{ always() }} + run: moodle-plugin-ci grunt --max-lint-warnings 0 + + - name: PHPUnit tests + if: ${{ always() }} + run: moodle-plugin-ci phpunit --coverage-clover + + - name: Behat features + if: ${{ always() }} + run: moodle-plugin-ci behat --profile chrome + + - name: Convert Coverage (clover2lcov) + uses: andstor/clover2lcov-action@v1 + if: ${{ always() }} + with: + src: ./coverage.xml + dst: ./coverage/lcov.info + + - name: Coveralls Parallel + if: ${{ always() }} + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + flag-name: run-${{ matrix.test_number }} + parallel: true + + finish: + needs: test + if: always() + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1c0029 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.vscode + +*.code-workspace + +.idea/ + +.history diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9879a04 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,51 @@ +language: php + +addons: + postgresql: "9.5" + +services: + - mysql + - postgresql + - docker + +cache: + directories: + - $HOME/.composer/cache + - $HOME/.npm + +php: + - 7.2 + - 7.3 + - 7.4 + +env: + global: + - MOODLE_BRANCH=MOODLE_39_STABLE + matrix: + - DB=pgsql + - DB=mysqli + +before_install: + - phpenv config-rm xdebug.ini + - cd ../.. + - composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3 + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci install + +script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci savepoints + - moodle-plugin-ci mustache + - moodle-plugin-ci grunt + - moodle-plugin-ci phpdoc + - moodle-plugin-ci phpunit --coverage-clover + - moodle-plugin-ci behat + +after_success: + - moodle-plugin-ci coveralls-upload diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..76074b6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org). + +## [Unreleased] + +## [0.1.0] - 2021-07-22 +### Added +- Ability to register manual issues. +- Modular external API for creating QTracker blocks. +- Backup and Restore API. +- Privacy API is implemented in order to comply with the [General Data Protection Regulation](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation) (GDPR). + +[Unreleased]: https://github.com/KQMATH/moodle-mod_capquiz/compare/v0.1.0...HEAD + +[0.1.1]: https://github.com/KQMATH/moodle-mod_capquiz/compare/v0.1.0...v0.1.1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5582eb6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at hasc@ntnu.no. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..02deebe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,66 @@ +capquiz# How to contribute + +Thank you for your interest in contributing to the QTracker project. + +## How can you help? + +* Report issues +* Fix typos and grammar +* Add new rules +* Improve existing rules and workflow + +## Best practices + +- Bugfixes should only contain changes that are related to the purpose of the bug. +- Description should contain an explanation for the proposed changes. +- It's recommended to consult feature requests with the team before starting implementation +- Send us an email if you need assistance with any work. + +## Making changes + +* Fork this repository on [GitHub](https://github.com/KQMATH/moodle-local_qtracker). +* All development work should follow the [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. +* Make sure you have added the necessary tests for your changes. +* If applicable, include a link to the issue in the commit message body. + +## Submitting changes + +* Push your changes to a topic branch in your fork of the repository. +* Submit a pull request to the repository in the [KQMATH GitHub organization](https://github.com/KQMATH) +and choose branch you want to patch (usually develop). +* Add detail about the change to the pull request including screenshots + if the change affects the UI. + +## Reviewing changes + +* After submitting a pull request, one of QTracker team members will review it. +* Changes may be requested to conform to our style guide and internal + requirements. +* When the changes are approved and all tests are passing, a QTracker team + member will merge them. +* Note: if you have write access to the repository, do not directly merge pull + requests. Let another team member review your pull request and approve it. + +## Style guide + +* This repository uses [Markdown](https://daringfireball.net/projects/markdown/) syntax. +* The preferred spelling of English words is the [American + English](https://en.wikipedia.org/wiki/American_English) (e.g. behavior, not + behaviour). +* The required coding style is the [Moodle cooding style](https://docs.moodle.org/dev/Coding_style). + + +## License + +By contributing to this repository you agree that all contributions are subject to the +GNU General Public License v3.0 under thepublic domain. +See [LICENSE](https://github.com/KQMATH/moodle-local_qtracker/blob/master/LICENSE) +file for more information. + +## Review and release process + +* Each addition and rule change is discussed and reviewed internally by QTracker + core team. +* When contents are updated, [CHANGELOG.md](/CHANGELOG.md) file is updated and a + new tag is released. Repository follows [semantic versioning](http://semver.org/). +* The [official release](https://moodle.org/plugins/local_qtracker) at Moodle must be manually updated accordingly. diff --git a/README.md b/README.md index b071092..26e8351 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ -# moodle-local_questiontracker +# moodle-local_qtracker +[![build](https://github.com/KQMATH/moodle-local_qtracker/actions/workflows/moodle-ci.yml/badge.svg?branch=master)](https://github.com/KQMATH/moodle-local_qtracker/actions/workflows/moodle-ci.yml) + :bug: Local Moodle plugin providing issue tracking for Moodle questions. + +The QTracker system allows students to comment and ask questions about individual questions in a quiz. The idea and design follows the principles of issue tracker systems. + +In addition to the QTracker module, a separate block-type module is needed to add the interface to a given activity type. Such block modules have been made for ++ Core Quiz - [Quiz QTracker](https://github.com/KQMATH/moodle-block_quizqtracker) ++ CAPQuiz - [CAPQuiz QTracker](https://github.com/KQMATH/moodle-block_capquizqtracker) + +The QTracker functionality is normally accessed via the appropriate block plugin. + +## Documentation +Documentation is available [here](https://github.com/KQMATH/oodle-local_qtracker/wiki), including [installation instructions](https://github.com/KQMATH/moodle-local_qtracker/wiki/Installation-instructions). + +## Feedback: +**Project lead:** Hans Georg Schaathun + +**Developer:** [André Storhaug](https://github.com/andstor) + +## License +QTracker is licensed under the [GNU General Public, License Version 3](https://github.com/KQMATH/moodle-local_qtracker/LICENSE). + +## Related + +- [CAPQuiz](https://moodle.org/plugins/mod_capquiz) - Computer adaptive practice activity module for Moodle +- [ShortMath](https://moodle.org/plugins/qtype_shortmath) - Moodle question type for writing mathematical expressions using MathQuill +- [JazzQuiz](https://moodle.org/plugins/mod_jazzquiz) - Moodle activity module, letting the teacher run a preplanned quiz with the power of improvisation diff --git a/amd/build/api_helpers.min.js b/amd/build/api_helpers.min.js new file mode 100644 index 0000000..f92b2ee --- /dev/null +++ b/amd/build/api_helpers.min.js @@ -0,0 +1,2 @@ +define ("local_qtracker/api_helpers",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.loadIssuesData=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function c(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function d(a){return function(){var b=this,d=arguments;return new Promise(function(e,f){var i=a.apply(b,d);function g(a){c(i,e,f,g,h,"next",a)}function h(a){c(i,e,f,g,h,"throw",a)}g(void 0)})}}var e=function(){var a=d(regeneratorRuntime.mark(function a(c){var d,e,f,g=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=1.\n\n/**\n * @module local_qtracker/api_helpers\n * @package local_qtracker\n * @author André Storhaug \n * @copyright 2021 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from \"core/ajax\";\n\n/**\n * TODO: dynamically load \"from-to\" and use limit.\n * @param {*} criteria\n * @param {*} from\n * @param {*} limit\n * @returns\n */\nexport const loadIssuesData = async (criteria, from = 0, limit = 100) => {\n let issuesData = await Ajax.call([{\n methodname: 'local_qtracker_get_issues',\n args: {\n criteria: criteria,\n from: from,\n limit: limit,\n }\n }])[0];\n\n return issuesData;\n}\n\n"],"file":"api_helpers.min.js"} \ No newline at end of file diff --git a/amd/build/block_form_manager.min.js b/amd/build/block_form_manager.min.js new file mode 100644 index 0000000..70284c1 --- /dev/null +++ b/amd/build/block_form_manager.min.js @@ -0,0 +1,2 @@ +define ("local_qtracker/block_form_manager",["jquery","core/str","core/templates","core/ajax","local_qtracker/issue","local_qtracker/issue_manager"],function(a,b,c,d,e,f){var g={SLOT:"[name=\"slot\"]",SLOT_SELECT_OPTION:"[name=\"slot\"] option",TITLE:"[name=\"issuetitle\"]",DESCRIPTION:"[name=\"issuedescription\"]",SUBMIT_BUTTON:"button[type=\"submit\"]",DELETE_BUTTON:"#qtracker-delete"},h=[g.TITLE,g.DESCRIPTION],i=null,j=function(b,c,d){this.contextid=d;this.form=a(b);this.form.closest(".card-text").prepend("");this.issueManager=new f;this.init(JSON.parse(c))};j.prototype.form=null;j.prototype.contextid=-1;j.prototype.issueid=null;j.prototype.issues=[];j.prototype.issueManager=null;j.prototype.init=function(){var b=this,c=0.\n\n/**\n * Manager for a Question Tracker Block form.\n *\n * @module local_qtracker/BlockFormManager\n * @class BlockFormManager\n * @package local_qtracker\n * @author André Storhaug \n * @copyright 2020 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/str', 'core/templates', 'core/ajax', 'local_qtracker/issue', 'local_qtracker/issue_manager'],\n function ($, Str, Templates, Ajax, Issue, IssueManager) {\n var SELECTORS = {\n SLOT: '[name=\"slot\"]',\n SLOT_SELECT_OPTION: '[name=\"slot\"] option',\n TITLE: '[name=\"issuetitle\"]',\n DESCRIPTION: '[name=\"issuedescription\"]',\n SUBMIT_BUTTON: 'button[type=\"submit\"]',\n DELETE_BUTTON: '#qtracker-delete',\n };\n\n let VALIDATION_ELEMENTS = [\n SELECTORS.TITLE,\n SELECTORS.DESCRIPTION,\n ];\n\n var NOTIFICATION_DURATION = 7500;\n var notificationTimeoutHandle = null;\n\n /**\n * Constructor\n *\n * @param {String} selector used to find triggers for the new group modal.\n * @param {string} issueids\n * @param {int} contextid\n *\n * Each call to init gets it's own instance of this class.\n */\n var BlockFormManager = function (selector, issueids, contextid) {\n this.contextid = contextid;\n this.form = $(selector);\n this.form.closest('.card-text').prepend('');\n this.issueManager = new IssueManager();\n this.init(JSON.parse(issueids));\n };\n\n /**\n * @var {Form} form\n * @private\n */\n BlockFormManager.prototype.form = null;\n\n /**\n * @var {int} contextid\n * @private\n */\n BlockFormManager.prototype.contextid = -1;\n\n /**\n * @var {int} issueid\n * @private\n */\n BlockFormManager.prototype.issueid = null;\n\n /**\n * @var {issue[]} issues\n * @private\n */\n BlockFormManager.prototype.issues = [];\n\n /**\n * @var {issue_manager} issueManager\n * @private\n */\n BlockFormManager.prototype.issueManager = null;\n\n /**\n * Initialise the class.\n *\n * @param {*[]} issueids selector used to find triggers for the new question issue.\n * @private\n */\n BlockFormManager.prototype.init = function (issueids = []) {\n // Init all slots\n let slots = $(SELECTORS.SLOT_SELECT_OPTION);\n if (slots.length == 0) {\n slots = $(SELECTORS.SLOT);\n }\n slots.map((index, option) => {\n let issue = new Issue(null, parseInt(option.value), this.contextid);\n issue.isSaved = false;// ChangeState(Issue.STATES.NEW);\n this.issueManager.addIssue(issue);\n });\n\n\n this.issueManager.loadIssues(issueids).then(() => {\n\n var formData = new FormData(this.form[0]);\n this.issueManager.setActiveIssue(parseInt(formData.get('slot')));\n\n this.reflectFormState();\n\n // Issue title event listener.\n let titleElement = this.form.find(SELECTORS.TITLE);\n titleElement.change((event) => {\n this.issueManager.getActiveIssue().setTitle(event.target.value);\n });\n /* $(document).on(qtrackerEvents.CHANGED_SLOT_BLOCK_FORM, (event, value) => {\n titleElement.val(value);\n }); */\n\n // Issue description event listener.\n let descriptionElement = this.form.find(SELECTORS.DESCRIPTION);\n descriptionElement.change((event) => {\n this.issueManager.getActiveIssue().setDescription(event.target.value);\n });\n /* $(document).on(qtrackerEvents.CHANGED_SLOT_BLOCK_FORM, (event, value) => {\n descriptionElement.val(value)\n }); */\n\n //\n\n // Load existing issues.\n var slotElement = this.form.find(SELECTORS.SLOT);\n slotElement.change(this.handleSlotChange.bind(this));\n\n this.form.on('submit', this.submitFormAjax.bind(this));\n\n }).catch((error) => {\n console.error(error);\n });\n };\n\n BlockFormManager.prototype.handleSlotChange = function (e) {\n this.issueManager.setActiveIssue(parseInt(e.target.value));\n this.reflectFormState();\n this.resetValidation();\n };\n\n BlockFormManager.prototype.reflectFormState = function () {\n let issue = this.issueManager.getActiveIssue();\n if (issue.isSaved === true) { // State === Issue.STATES.EXISTING) {\n this.toggleDeleteButton(true);\n this.toggleUpdateButton(true);\n } else if (issue.isSaved === false) { // State === Issue.STATES.NEW) {\n this.clearForm();\n }\n\n this.restoreForm();\n };\n\n /**\n * @method handleFormSubmissionResponse\n * @param response\n * @private\n */\n BlockFormManager.prototype.handleFormSubmissionResponse = function (response) {\n\n // TODO: handle response.status === false\n // TODO: handle response.warning ...\n\n // We could trigger an event instead.\n // Yuk.\n Y.use('moodle-core-formchangechecker', function () {\n M.core_formchangechecker.reset_form_dirty_state();\n });\n // Document.location.reload();\n\n this.issueManager.getActiveIssue().setId(response.issueid);\n };\n\n /**\n * @method handleFormSubmissionFailure\n * @param response\n * @private\n */\n BlockFormManager.prototype.handleFormSubmissionFailure = function (response) {\n // Oh noes! Epic fail :(\n // Ah wait - this is normal. We need to re-display the form with errors!\n console.error(\"An error occured\");\n console.error(response);\n };\n\n BlockFormManager.prototype.clearForm = function () {\n // Remove delete button.\n this.form.find('#qtracker-delete').remove();\n this.resetValidation();\n Str.get_string('submitnewissue', 'local_qtracker').then(function (string) {\n this.form.find('button[type=\"submit\"]').html(string);\n }.bind(this));\n };\n\n BlockFormManager.prototype.restoreForm = function () {\n let issue = this.issueManager.getActiveIssue();\n this.form.find('[name=\"issuetitle\"]').val(issue.getTitle());\n this.form.find('[name=\"issuedescription\"]').val(issue.getDescription());\n\n };\n\n /**\n * @method editIssue\n * @private\n */\n BlockFormManager.prototype.editIssue = function () {\n var formData = new FormData(this.form[0]);\n Ajax.call([{\n methodname: 'local_qtracker_edit_issue',\n args: {\n issueid: this.issueManager.getActiveIssue().getId(),\n issuetitle: formData.get('issuetitle'),\n issuedescription: formData.get('issuedescription'),\n },\n done: function (response) {\n Str.get_string('issueupdated', 'local_qtracker').then(function (string) {\n let notification = {\n message: string,\n announce: true,\n type: \"success\",\n };\n this.notify(notification);\n }.bind(this));\n this.handleFormSubmissionResponse(response);\n }.bind(this),\n fail: this.handleFormSubmissionFailure.bind(this)\n }]);\n };\n\n /**\n * @method editIssue\n * @private\n */\n BlockFormManager.prototype.deleteIssue = function () {\n Ajax.call([{\n methodname: 'local_qtracker_delete_issue',\n args: {\n issueid: this.issueManager.getActiveIssue().getId(),\n },\n done: function () {\n Str.get_string('issuedeleted', 'local_qtracker').then(function (string) {\n let notification = {\n message: string,\n announce: true,\n type: \"success\",\n };\n this.notify(notification);\n }.bind(this));\n this.issueManager.getActiveIssue().isSaved = false;// ChangeState(Issue.STATES.NEW);;\n this.clearForm();\n }.bind(this),\n fail: this.handleFormSubmissionFailure.bind(this)\n }]);\n };\n\n /**\n * @method handleFormSubmissionFailure\n * @private\n */\n BlockFormManager.prototype.createIssue = function () {\n var formData = new FormData(this.form[0]);\n // Now we can continue...\n Ajax.call([{\n methodname: 'local_qtracker_new_issue',\n args: {\n qubaid: formData.get('qubaid'),\n slot: formData.get('slot'),\n contextid: this.contextid,\n issuetitle: formData.get('issuetitle'),\n issuedescription: formData.get('issuedescription'),\n },\n done: function (response) {\n Str.get_string('issuecreated', 'local_qtracker').then(function (string) {\n let notification = {\n message: string,\n announce: true,\n type: \"success\",\n };\n this.notify(notification);\n }.bind(this));\n this.issueManager.getActiveIssue().isSaved = true;// ChangeState(Issue.STATES.EXISTING)\n // This.setAction(ACTION.EDITISSUE);\n // TODO: add delete button.\n this.toggleUpdateButton(true);\n this.toggleDeleteButton(true);\n\n this.handleFormSubmissionResponse(response);\n }.bind(this),\n fail: this.handleFormSubmissionFailure.bind(this)\n }]);\n };\n\n /**\n * Cancel any typing pause timer.\n */\n BlockFormManager.prototype.cancelNotificationTimer = function () {\n if (notificationTimeoutHandle) {\n clearTimeout(notificationTimeoutHandle);\n }\n notificationTimeoutHandle = null;\n };\n\n BlockFormManager.prototype.notify = function (notification) {\n notification = $.extend({\n closebutton: true,\n announce: true,\n type: 'error',\n extraclasses: \"show\",\n }, notification);\n\n let types = {\n 'success': 'core/notification_success',\n 'info': 'core/notification_info',\n 'warning': 'core/notification_warning',\n 'error': 'core/notification_error',\n };\n\n this.cancelNotificationTimer();\n\n let template = types[notification.type];\n Templates.render(template, notification)\n .then((html, js) => {\n $('#qtracker-notifications').html(html);\n Templates.runTemplateJS(js);\n\n notificationTimeoutHandle = setTimeout(() => {\n $('#qtracker-notifications').find('.alert').alert('close');\n }, NOTIFICATION_DURATION);\n })\n .catch((error) => {\n console.error(error);\n throw error;\n });\n };\n /**\n * @method handleFormSubmissionFailure\n * @param {boolean} show\n * @private\n */\n BlockFormManager.prototype.toggleUpdateButton = function (show) {\n if (show) {\n Str.get_string('update', 'core').then(function (updateStr) {\n this.form.find(SELECTORS.SUBMIT_BUTTON).html(updateStr);\n }.bind(this));\n } else {\n Str.get_string('submitnewissue', 'local_qtracker').then(function (updateStr) {\n this.form.find(SELECTORS.SUBMIT_BUTTON).html(updateStr);\n }.bind(this));\n }\n };\n /**\n * @method handleFormSubmissionFailure\n * @param {boolean} show\n * @private\n */\n BlockFormManager.prototype.toggleDeleteButton = function (show) {\n const context = {\n type: \"button\",\n classes: \"col-auto\",\n label: \"Delete\",\n id: \"qtracker-delete\",\n };\n\n let deleteButton = this.form.find(SELECTORS.DELETE_BUTTON);\n if (deleteButton.length == 0 && show) {\n Templates.render('local_qtracker/button', context)\n .then(function (html, js) {\n var container = this.form.find('button').closest(\".form-row\");\n Templates.appendNodeContents(container, html, js);\n this.form.find('#qtracker-delete').on('click', function () {\n this.deleteIssue();\n }.bind(this));\n }.bind(this));\n } else {\n if (show) {\n deleteButton.show();\n } else {\n deleteButton.hide();\n }\n }\n };\n\n /**\n * @method handleFormSubmissionFailure\n * @param {string} newaction\n * @private\n */\n BlockFormManager.prototype.setAction = function (newaction) {\n\n this.form.data('action', newaction);\n };\n\n /**\n * Private method\n *\n * @method submitFormAjax\n * @private\n * @param {Event} e Form submission event.\n */\n BlockFormManager.prototype.submitFormAjax = function (e) {\n // We don't want to do a real form submission.\n e.preventDefault();\n e.stopPropagation();\n\n if (!this.validateForm()) {\n return;\n }\n\n\n if (this.issueManager.getActiveIssue().isSaved === true) {\n this.editIssue();\n } else {\n this.createIssue();\n }\n /*\n Var state = this.issueManager.getActiveIssue().getState();\n switch (state) {\n case Issue.STATES.NEW:\n this.createIssue();\n break;\n case Issue.STATES.EXISTING:\n this.editIssue();\n break;\n case Issue.STATES.DELETED:\n this.issueManager.getActiveIssue().changeState(Issue.STATES.NEW)\n this.createIssue();\n break;\n default:\n break;\n }*/\n };\n\n BlockFormManager.prototype.validateForm = function () {\n let valid = true;\n VALIDATION_ELEMENTS.forEach(selector => {\n let element = this.form.find(selector);\n if (element.val() != \"\" && element.prop(\"validity\").valid) {\n element.removeClass(\"is-invalid\").addClass(\"is-valid\");\n } else {\n element.removeClass(\"is-valid\").addClass(\"is-invalid\");\n valid = false;\n }\n });\n return valid;\n };\n\n BlockFormManager.prototype.resetValidation = function () {\n VALIDATION_ELEMENTS.forEach(selector => {\n let element = this.form.find(selector);\n element.removeClass(\"is-invalid\").removeClass(\"is-valid\");\n });\n };\n\n /**\n * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed.\n *\n * @method submitForm\n * @param {Event} e Form submission event.\n * @private\n */\n BlockFormManager.prototype.submitForm = function (e) {\n e.preventDefault();\n this.form.submit();\n };\n\n return /** @alias module:local_qtracker/BlockFormManager */ {\n\n /**\n * Initialise the module.\n *\n * @method init\n * @param {string} selector The selector used to find the form for to use for this module.\n * @param {string} issueids The ids of existing issues to load.\n * @param {int} contextid\n * @return {BlockFormManager}\n */\n init: function (selector, issueids, contextid) {\n return new BlockFormManager(selector, issueids, contextid);\n }\n };\n });\n"],"file":"block_form_manager.min.js"} \ No newline at end of file diff --git a/amd/build/dropdown.min.js b/amd/build/dropdown.min.js new file mode 100644 index 0000000..84d7b51 --- /dev/null +++ b/amd/build/dropdown.min.js @@ -0,0 +1,2 @@ +define ("local_qtracker/dropdown",["exports","jquery","core/templates","core/str","local_qtracker/dropdown_events","local_qtracker/api_helpers"],function(a,b,c,d,f,g){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=h(b);c=h(c);f=h(f);function h(a){return a&&a.__esModule?a:{default:a}}function i(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function j(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){i(h,d,e,f,g,"next",a)}function g(a){i(h,d,e,f,g,"throw",a)}f(void 0)})}}function k(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function l(a,b){for(var c=0,d;c").addClass("dropdown-item disabled").attr("data-region","item");a.next=6;return(0,d.get_string)("noitems","local_qtracker");case 6:a.t1=a.sent;g=a.t0.html.call(a.t0,a.t1).prop("outerHTML");e.push(g);a.next=12;break;case 11:f.forEach(function(a,d){var f=(0,b.default)("
").addClass("dropdown-item").addClass(function(){if(c.isActiveItem(d)){return"active"}}).attr("data-value",d).attr("data-region","item").html(a).prop("outerHTML");e.push(f)});case 12:return a.abrupt("return",e);case 13:case"end":return a.stop();}}},a,this)}));return function generateItems(){return a.apply(this,arguments)}}()},{key:"renderItems",value:function(){var a=j(regeneratorRuntime.mark(function a(){var b;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:this.empty();a.next=3;return this.generateItems();case 3:b=a.sent;this.getDropdown().append(b);case 5:case"end":return a.stop();}}},a,this)}));return function renderItems(){return a.apply(this,arguments)}}()},{key:"setItems",value:function setItems(a){var b=this,c=1.\n\n/**\n * Manager for managing table of questions with issues.\n *\n * @module local_qtracker/Dropdown\n * @class Dropdown\n * @package local_qtracker\n * @author André Storhaug \n * @copyright 2021 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport $ from 'jquery';\nimport Templates from 'core/templates';\nimport { get_string as getString } from 'core/str';\nimport DropdownEvents from 'local_qtracker/dropdown_events';\nimport { loadIssuesData } from 'local_qtracker/api_helpers';\n\nvar SELECTORS = {\n DROPDOWN: '[data-region=\"dropdown\"]',\n SEARCH: 'input[type=\"search\"]',\n ITEM: '[data-region=\"item\"]',\n};\n\n/**\n * Constructor\n * @constructor\n * @param {String} selector used to find triggers for the new group modal.\n * @param {int} contextid\n *\n * Each call to init gets it's own instance of this class.\n */\nclass Dropdown {\n loading = null;\n root = null;\n items = new Map();\n activeItems = new Map();\n isSearching = false;\n\n constructor(root, search = true) {\n this.root = $(root); // Root element dropdown-menu\n this.dropdown = this.root.find(SELECTORS.DROPDOWN);\n this.search = search;\n\n this.render = this.renderItems.bind(this);\n this.registerEventListeners = this.registerEventListeners.bind(this);\n\n if (this.search) {\n this.registerEventListeners();\n }\n }\n\n\n\n /**\n * Get the dropdown element of this dropdown.\n *\n * @method getDropdown\n * @return {object} jQuery object\n */\n getDropdown() {\n return this.dropdown;\n };\n\n /**\n * Get the dropdown element of this dropdown.\n *\n * @method getDropdown\n * @return {object} jQuery object\n */\n getRoot() {\n return this.root;\n };\n\n /**\n * Set up all of the event handling for the modal.\n *\n * @method registerEventListeners\n */\n registerEventListeners() {\n // Handle the clicking of an item.\n this.getDropdown().on('click', SELECTORS.ITEM, function (e) {\n let element = $(e.currentTarget);\n if (element.hasClass(\"disabled\")) {\n return;\n }\n var clickEvent = $.Event(DropdownEvents.click);\n this.getRoot().trigger(clickEvent, [element]);\n\n if (!clickEvent.isDefaultPrevented()) {\n e.preventDefault();\n }\n }.bind(this));\n\n this.registerSearchListener();\n }\n\n\n /**\n * Register a listener to close the dialogue when the save button is pressed.\n *\n * @method registerSearch\n */\n registerSearchListener() {\n\n this.getDropdown().find(SELECTORS.SEARCH).on('input', function (e) {\n\n let str = $(e.target).val();\n var searchEvent = $.Event(DropdownEvents.search, str);\n this.getRoot().trigger(searchEvent, [str]);\n\n if (!searchEvent.isDefaultPrevented()) {\n e.preventDefault();\n if (str.length > 0) {\n this.isSearching = true;\n } else {\n this.isSearching = false;\n }\n this.renderItems();\n }\n }.bind(this));\n };\n\n\n getActiveItems() {\n\n return this.activeItems;\n }\n\n setItemStatus(key, active = true) {\n if (active) {\n let item = this.getItems().get(key);\n this.activeItems.set(key, item);\n } else {\n this.activeItems.delete(key);\n }\n }\n\n reset() {\n this.isSearching = false;\n this.getDropdown().find(SELECTORS.SEARCH).val(\"\");\n this.renderItems()\n }\n\n async render() {\n let context = {\n trigger: {\n \"key\": \"fa-times\",\n \"title\": \"Close\",\n \"alt\": \"Close pane\",\n \"extraclasses\": \"\",\n \"unmappedIcon\": false\n }\n };\n //let self = this;\n await Templates.render('local_qtracker/dropdown', context).then((html, js) => {\n Templates.replaceNodeContents(this.getDropdown(), html, js);\n });\n }\n\n async generateItems() {\n let elements = [];\n let items = this.isSearching ? this.getItems() : this.getActiveItems();\n if (items.size === 0) {\n let element = $('
')\n .addClass(\"dropdown-item disabled\")\n .attr(\"data-region\", 'item')\n .html(await getString('noitems', 'local_qtracker'))\n .prop('outerHTML');\n elements.push(element)\n } else {\n // map ;; index(id), html\n items.forEach((html, key) => {\n let element = $('
')\n .addClass(\"dropdown-item\")\n .addClass(() => {\n if (this.isActiveItem(key)) {\n return \"active\";\n }\n })\n .attr(\"data-value\", key)\n .attr(\"data-region\", 'item')\n .html(html).prop('outerHTML');\n elements.push(element)\n });\n }\n return elements;\n }\n\n async renderItems() {\n this.empty();\n let elements = await this.generateItems();\n this.getDropdown().append(elements);\n //{{{text}}}\n }\n\n /**\n *\n * @param {*} items tuples [id, html]\n * @param {*} active\n */\n setItems(items, active = false) {\n if (active) {\n this.activeItems = new Map();\n items.forEach(item => this.activeItems.set(item[0], item[1]));\n } else {\n this.items = new Map();\n items.forEach(item => this.items.set(item[0], item[1]));\n }\n }\n\n getItems() {\n return this.items;\n }\n\n getAllItems() {\n return Array.prototype.concat(this.items, this.getActiveItems());\n }\n\n isActiveItem(key) {\n return this.getActiveItems().has(key);\n }\n\n async search(str) {\n if (str.length > 0) {\n this.isSearching = true;\n } else {\n this.isSearching = false;\n }\n\n let criteria = [];\n if (str.startsWith('#')) {\n let id = parseInt(str.substr(1));\n criteria.push({ key: 'id', value: id });\n } else {\n if (str.length > 2) str += \"%\"\n criteria.push({ key: 'title', value: str });\n }\n\n let issuesResponse = await loadIssuesData(criteria);\n let issues = issuesResponse.issues;\n this.setItems(issues);\n return issues;\n }\n\n setTitle(html) {\n $('.qtracker-sidebar-title').html(html);\n }\n\n setLoading(show = true) {\n if (show) {\n $('.qtracker-sidebar-content .loading').addClass(\"show\");\n this.loading = true;\n } else {\n $('.qtracker-sidebar-content .loading').removeClass(\"show\");\n this.loading = false;\n }\n }\n\n empty() {\n this.getDropdown().find(SELECTORS.ITEM).remove();\n }\n\n addTemplateItem(html, js) {\n Templates.appendNodeContents('.qtracker-sidebar-content .qtracker-items', html, js);\n }\n\n getElements() {\n return $('.qtracker-sidebar-content .qtracker-items').children();\n }\n\n}\n\nexport default Dropdown;\n"],"file":"dropdown.min.js"} \ No newline at end of file diff --git a/amd/build/dropdown_events.min.js b/amd/build/dropdown_events.min.js new file mode 100644 index 0000000..b85dff3 --- /dev/null +++ b/amd/build/dropdown_events.min.js @@ -0,0 +1,2 @@ +define ("local_qtracker/dropdown_events",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={search:"dropdown:search",click:"dropdown:click",change:"dropdown:change"};return a.default}); +//# sourceMappingURL=dropdown_events.min.js.map diff --git a/amd/build/dropdown_events.min.js.map b/amd/build/dropdown_events.min.js.map new file mode 100644 index 0000000..207c684 --- /dev/null +++ b/amd/build/dropdown_events.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/dropdown_events.js"],"names":["search","click","change"],"mappings":"0JASe,CACXA,MAAM,CAAE,iBADG,CAEXC,KAAK,CAAE,gBAFI,CAGXC,MAAM,CAAE,iBAHG,C","sourcesContent":["/**\n * Events for the drawer.\n *\n * @module local_qtracker/DropdownEvents\n * @package local_qtracker\n * @author André Storhaug \n * @copyright 2020 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n search: 'dropdown:search',\n click: 'dropdown:click',\n change: 'dropdown:change',\n\n};\n"],"file":"dropdown_events.min.js"} \ No newline at end of file diff --git a/amd/build/issue.min.js b/amd/build/issue.min.js new file mode 100644 index 0000000..ee5bd76 --- /dev/null +++ b/amd/build/issue.min.js @@ -0,0 +1,2 @@ +function asyncGeneratorStep(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function _asyncToGenerator(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){asyncGeneratorStep(h,d,e,f,g,"next",a)}function g(a){asyncGeneratorStep(h,d,e,f,g,"throw",a)}f(void 0)})}}define ("local_qtracker/issue",["jquery","core/str","core/ajax"],function(a,b,c){var d=function(){var a=0.\n\n/**\n * Module for representing a question issue.\n *\n * @module local_qtracker/Issue\n * @class Issue\n * @package local_qtracker\n * @author André Storhaug \n * @copyright 2020 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/str', 'core/ajax'], function ($, Str, Ajax) {\n\n /**\n * Constructor\n *\n * @param {int} id\n * @param {int} slot\n * @param {int} contextid\n *\n * Each call gets it's own instance of this class.\n */\n var Issue = function (id = null, slot = null, contextid) {\n this.id = id;\n this.slot = slot;\n this.contextid = contextid;\n };\n\n Issue.STATES = {\n NEW: \"new\",\n OPEN: \"open\",\n CLOSED: \"closed\",\n };\n\n /**\n * @var {int} id The id of this issue\n * @private\n */\n Issue.prototype.id = null;\n\n /**\n * @var {int} id The slot for this issue\n * @private\n */\n Issue.prototype.slot = null;\n\n /**\n * @var {string} title The title for this issue\n * @private\n */\n Issue.prototype.title = \"\";\n\n /**\n * @var {string} title The description for this issue\n * @private\n */\n Issue.prototype.description = \"\";\n\n Issue.prototype.contextid = null;\n\n Issue.prototype.isSaved = false;\n\n Issue.prototype.state = Issue.STATES.NEW;\n\n /**\n * Initialise the class.\n *\n * @private\n * @param {int} id\n */\n Issue.prototype.setId = function (id) {\n this.id = id;\n };\n\n /**\n * Initialise the class.\n *\n * @param {String} selector used to find triggers for the new group modal.\n * @private\n * @return {Promise}\n */\n Issue.prototype.getId = function () {\n return this.id;\n };\n\n Issue.prototype.getSlot = function () {\n return this.slot;\n };\n\n Issue.prototype.getTitle = function () {\n return this.title;\n };\n\n Issue.prototype.setTitle = function (title) {\n this.title = title;\n };\n\n Issue.prototype.getDescription = function () {\n return this.description;\n };\n\n\n Issue.prototype.setDescription = function (description) {\n this.description = description;\n };\n\n Issue.prototype.changeState = function (state) {\n this.state = state;\n };\n\n Issue.prototype.getState = function () {\n return this.state;\n };\n\n Issue.prototype.getContextid = function () {\n return this.contextid;\n };\n\n /**\n * @return {Promise}\n * @param {int} id\n */\n Issue.loadData = function (id) {\n return Ajax.call([\n { methodname: 'local_qtracker_get_issue', args: { issueid: id } }\n ])[0];\n };\n\n Issue.load = async function (id) {\n let data = await Issue.loadData(id);\n let issueData = data.issue;\n let issue = new Issue(issueData.id, issueData.slot, issueData.contextid);\n issue.setTitle(issueData.title);\n issue.setDescription(issueData.description);\n return issue;\n }\n\n Issue.prototype.save = async function () {\n let result = await Ajax.call([{\n methodname: 'local_qtracker_edit_issue',\n args: {\n issueid: this.getId(),\n issuetitle: this.getTitle(),\n issuedescription: this.getDescription(),\n },\n }])[0];\n return result\n }\n\n return Issue;\n});\n"],"file":"issue.min.js"} \ No newline at end of file diff --git a/amd/build/issue_comment_controls.min.js b/amd/build/issue_comment_controls.min.js new file mode 100644 index 0000000..72af091 --- /dev/null +++ b/amd/build/issue_comment_controls.min.js @@ -0,0 +1,2 @@ +function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("local_qtracker/issue_comment_controls",["exports","jquery","core/str","core/modal_factory","core/modal_events"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=h(b);c=g(c);d=h(d);e=h(e);function f(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;f=function(){return a};return a}function g(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=f();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var g=d?Object.getOwnPropertyDescriptor(a,e):null;if(g&&(g.get||g.set)){Object.defineProperty(c,e,g)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function h(a){return a&&a.__esModule?a:{default:a}}function i(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function j(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){i(h,d,e,f,g,"next",a)}function g(a){i(h,d,e,f,g,"throw",a)}f(void 0)})}}function k(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function l(a,b){for(var c=0,d;c").attr({type:"hidden",name:"deletecommentid",value:this.commentid}).appendTo(c);c.submit()}.bind(this));case 10:case"end":return a.stop();}}},a,this)}));return function registerDeleteButtonListener(){return a.apply(this,arguments)}}()},{key:"registerNotifyButtonListener",value:function(){var a=j(regeneratorRuntime.mark(function a(){var f,g,h,i;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:f=(0,b.default)("#comment_message_"+this.commentid);g=[{key:"confirm",component:"local_qtracker"},{key:"confirmsendcomment",component:"local_qtracker"},{key:"sendcomment",component:"local_qtracker"}];a.next=4;return c.get_strings(g);case 4:h=a.sent;a.next=7;return d.default.create({type:d.default.types.SAVE_CANCEL,title:h[2],body:h[1]},f);case 7:i=a.sent;i.setSaveButtonText(h[0]);i.getRoot().on(e.default.save,function(a){a.preventDefault();var c=(0,b.default)("#comment_form_"+this.commentid);(0,b.default)("").attr({type:"hidden",name:"notifycommentid",value:this.commentid}).appendTo(c);c.submit()}.bind(this));case 10:case"end":return a.stop();}}},a,this)}));return function registerNotifyButtonListener(){return a.apply(this,arguments)}}()}]);return a}();a.default=n;return a.default}); +//# sourceMappingURL=issue_comment_controls.min.js.map diff --git a/amd/build/issue_comment_controls.min.js.map b/amd/build/issue_comment_controls.min.js.map new file mode 100644 index 0000000..2984dee --- /dev/null +++ b/amd/build/issue_comment_controls.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/issue_comment_controls.js"],"names":["IssueCommentControls","commentid","init","registerDeleteButtonListener","registerNotifyButtonListener","trigger","strObj","key","component","Str","get_strings","strings","ModalFactory","create","type","types","SAVE_CANCEL","title","body","modal","setSaveButtonText","getRoot","on","ModalEvents","save","e","preventDefault","form","attr","name","value","appendTo","submit","bind"],"mappings":"+fAyBA,OACA,OACA,OACA,O,4tCASMA,CAAAA,C,YACF,WAAYC,CAAZ,CAAuB,WACnB,KAAKA,SAAL,CAAiBA,CAAjB,CACA,KAAKC,IAAL,EACH,C,gKAGG,KAAKC,4BAAL,GACA,KAAKC,4BAAL,G,0SAKIC,C,CAAU,cAAE,mBAAqB,KAAKJ,SAA5B,C,CACVK,C,CAAS,CACT,CACIC,GAAG,CAAE,SADT,CAEIC,SAAS,CAAE,gBAFf,CADS,CAKT,CACID,GAAG,CAAE,sBADT,CAEIC,SAAS,CAAE,gBAFf,CALS,CAST,CACID,GAAG,CAAE,eADT,CAEIC,SAAS,CAAE,gBAFf,CATS,C,gBAeOC,CAAAA,CAAG,CAACC,WAAJ,CAAgBJ,CAAhB,C,QAAhBK,C,uBAEcC,WAAaC,MAAb,CAAoB,CAClCC,IAAI,CAAEF,UAAaG,KAAb,CAAmBC,WADS,CAElCC,KAAK,CAAEN,CAAO,CAAC,CAAD,CAFoB,CAGlCO,IAAI,CAAEP,CAAO,CAAC,CAAD,CAHqB,CAApB,CAIfN,CAJe,C,QAAdc,C,QAMJA,CAAK,CAACC,iBAAN,CAAwBT,CAAO,CAAC,CAAD,CAA/B,EACAQ,CAAK,CAACE,OAAN,GAAgBC,EAAhB,CAAmBC,UAAYC,IAA/B,CAAqC,SAAUC,CAAV,CAAa,CAE9CA,CAAC,CAACC,cAAF,GACA,GAAIC,CAAAA,CAAI,CAAG,cAAE,iBAAoB,KAAK1B,SAA3B,CAAX,CACA,cAAE,SAAF,EAAa2B,IAAb,CAAkB,CACdd,IAAI,CAAE,QADQ,CAEde,IAAI,CAAE,iBAFQ,CAGdC,KAAK,CAAE,KAAK7B,SAHE,CAAlB,EAIG8B,QAJH,CAIYJ,CAJZ,EAKAA,CAAI,CAACK,MAAL,EACH,CAVoC,CAUnCC,IAVmC,CAU9B,IAV8B,CAArC,E,mUAeI5B,C,CAAU,cAAE,oBAAsB,KAAKJ,SAA7B,C,CACVK,C,CAAS,CACT,CACIC,GAAG,CAAE,SADT,CAEIC,SAAS,CAAE,gBAFf,CADS,CAKT,CACID,GAAG,CAAE,oBADT,CAEIC,SAAS,CAAE,gBAFf,CALS,CAST,CACID,GAAG,CAAE,aADT,CAEIC,SAAS,CAAE,gBAFf,CATS,C,gBAeOC,CAAAA,CAAG,CAACC,WAAJ,CAAgBJ,CAAhB,C,QAAhBK,C,uBAEcC,WAAaC,MAAb,CAAoB,CAClCC,IAAI,CAAEF,UAAaG,KAAb,CAAmBC,WADS,CAElCC,KAAK,CAAEN,CAAO,CAAC,CAAD,CAFoB,CAGlCO,IAAI,CAAEP,CAAO,CAAC,CAAD,CAHqB,CAApB,CAIfN,CAJe,C,QAAdc,C,QAMJA,CAAK,CAACC,iBAAN,CAAwBT,CAAO,CAAC,CAAD,CAA/B,EACAQ,CAAK,CAACE,OAAN,GAAgBC,EAAhB,CAAmBC,UAAYC,IAA/B,CAAqC,SAAUC,CAAV,CAAa,CAE9CA,CAAC,CAACC,cAAF,GACA,GAAIC,CAAAA,CAAI,CAAG,cAAE,iBAAoB,KAAK1B,SAA3B,CAAX,CACA,cAAE,SAAF,EAAa2B,IAAb,CAAkB,CACdd,IAAI,CAAE,QADQ,CAEde,IAAI,CAAE,iBAFQ,CAGdC,KAAK,CAAE,KAAK7B,SAHE,CAAlB,EAIG8B,QAJH,CAIYJ,CAJZ,EAKAA,CAAI,CAACK,MAAL,EACH,CAVoC,CAUnCC,IAVmC,CAU9B,IAV8B,CAArC,E,4JAeOjC,C","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 * Class for handling question issue page.\n *\n * @module local_qtracker/QuestionIssuePage\n * @class QuestionIssuePage\n * @package local_qtracker\n * @author André Storhaug \n * @copyright 2021 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport $ from 'jquery';\nimport * as Str from 'core/str';\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\n\n/**\n * Constructor\n * @constructor\n * @param {int} commentid\n *\n * Each call gets it's own instance of this class.\n */\nclass IssueCommentControls {\n constructor(commentid) {\n this.commentid = commentid;\n this.init();\n }\n\n async init() {\n this.registerDeleteButtonListener()\n this.registerNotifyButtonListener()\n }\n\n async registerDeleteButtonListener() {\n\n let trigger = $('#comment_delete_' + this.commentid);\n let strObj = [\n {\n key: 'confirm',\n component: 'local_qtracker'\n },\n {\n key: 'confirmdeletecomment',\n component: 'local_qtracker'\n },\n {\n key: 'deletecomment',\n component: 'local_qtracker'\n }\n ];\n\n let strings = await Str.get_strings(strObj);\n\n let modal = await ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n title: strings[2],\n body: strings[1],\n }, trigger)\n\n modal.setSaveButtonText(strings[0])\n modal.getRoot().on(ModalEvents.save, function (e) {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n let form = $('#comment_form_' + this.commentid);\n $('').attr({\n type: \"hidden\",\n name: \"deletecommentid\",\n value: this.commentid,\n }).appendTo(form);\n form.submit();\n }.bind(this));\n }\n\n async registerNotifyButtonListener() {\n\n let trigger = $('#comment_message_' + this.commentid);\n let strObj = [\n {\n key: 'confirm',\n component: 'local_qtracker'\n },\n {\n key: 'confirmsendcomment',\n component: 'local_qtracker'\n },\n {\n key: 'sendcomment',\n component: 'local_qtracker'\n }\n ];\n\n let strings = await Str.get_strings(strObj);\n\n let modal = await ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n title: strings[2],\n body: strings[1],\n }, trigger)\n\n modal.setSaveButtonText(strings[0])\n modal.getRoot().on(ModalEvents.save, function (e) {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n let form = $('#comment_form_' + this.commentid);\n $('').attr({\n type: \"hidden\",\n name: \"notifycommentid\",\n value: this.commentid,\n }).appendTo(form);\n form.submit();\n }.bind(this));\n }\n}\n\n\nexport default IssueCommentControls;\n"],"file":"issue_comment_controls.min.js"} \ No newline at end of file diff --git a/amd/build/issue_manager.min.js b/amd/build/issue_manager.min.js new file mode 100644 index 0000000..7a48bd3 --- /dev/null +++ b/amd/build/issue_manager.min.js @@ -0,0 +1,2 @@ +function _slicedToArray(a,b){return _arrayWithHoles(a)||_iterableToArrayLimit(a,b)||_unsupportedIterableToArray(a,b)||_nonIterableRest()}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _iterableToArrayLimit(a,b){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(a)))return;var c=[],d=!0,e=!1,f=void 0;try{for(var g=a[Symbol.iterator](),h;!(d=(h=g.next()).done);d=!0){c.push(h.value);if(b&&c.length===b)break}}catch(a){e=!0;f=a}finally{try{if(!d&&null!=g["return"])g["return"]()}finally{if(e)throw f}}return c}function _arrayWithHoles(a){if(Array.isArray(a))return a}function _createForOfIteratorHelper(a){if("undefined"==typeof Symbol||null==a[Symbol.iterator]){if(Array.isArray(a)||(a=_unsupportedIterableToArray(a))){var b=0,c=function(){};return{s:c,n:function n(){if(b>=a.length)return{done:!0};return{done:!1,value:a[b++]}},e:function e(a){throw a},f:c}}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 d,e=!0,f=!1,g;return{s:function s(){d=a[Symbol.iterator]()},n:function n(){var a=d.next();e=a.done;return a},e:function e(a){f=!0;g=a},f:function f(){try{if(!e&&null!=d.return)d.return()}finally{if(f)throw g}}}}function _unsupportedIterableToArray(a,b){if(!a)return;if("string"==typeof a)return _arrayLikeToArray(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return _arrayLikeToArray(a,b)}function _arrayLikeToArray(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Manager for managing question issues.\n *\n * @module local_qtracker/IssueManager\n * @class IssueManager\n * @package local_qtracker\n * @author André Storhaug \n * @copyright 2020 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'local_qtracker/issue'], function ($, Issue) {\n\n /**\n * Constructor\n * @constructor\n * @param {String} selector used to find triggers for the new group modal.\n * @param {int} contextid\n *\n * Each call to init gets it's own instance of this class.\n */\n var IssueManager = function () { };\n\n /**\n * @var {Form} form\n * @private\n */\n IssueManager.prototype.issues = new Map();\n\n IssueManager.prototype.activeIssue = null;\n\n IssueManager.prototype.getActiveIssue = function () {\n return this.activeIssue;\n };\n\n IssueManager.prototype.setActiveIssue = function (slot) {\n let newIssue = this.getIssueBySlot(slot);\n this.activeIssue = newIssue;\n return newIssue;\n };\n\n /**\n * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed.\n *\n * @method submitForm\n * @param slot\n * @private\n * @return\n */\n IssueManager.prototype.getIssueBySlot = function (slot) {\n return this.issues.get(slot);\n };\n\n IssueManager.prototype.getIssueById = function (id) {\n for (const [slot, issue] of this.issues) {\n if (issue.getId() !== null && issue.getId() === id) {\n return issue;\n }\n }\n return false;\n };\n\n IssueManager.prototype.addIssue = function (issue) {\n this.issues.set(issue.getSlot(), issue);\n };\n\n IssueManager.prototype.loadIssues = function (issueids = []) {\n let promises = [];\n for (let i = 0; i < issueids.length; i++) {\n const id = issueids[i];\n let promise = Issue.loadData(id).then((response) => {\n let issue = this.getIssueBySlot(response.issue.slot);\n if (!issue) {\n issue = new Issue(response.issue.id, response.issue.slot);\n }\n issue.setId(response.issue.id);\n issue.setTitle(response.issue.title);\n issue.setDescription(response.issue.description);\n issue.isSaved = true;// ChangeState(Issue.STATES.EXISTING);\n this.addIssue(issue);\n });\n promises.push(promise);\n }\n return Promise.all(promises);\n };\n\n return IssueManager;\n});\n"],"file":"issue_manager.min.js"} \ No newline at end of file diff --git a/amd/build/question_issue_page.min.js b/amd/build/question_issue_page.min.js new file mode 100644 index 0000000..abc1c61 --- /dev/null +++ b/amd/build/question_issue_page.min.js @@ -0,0 +1,2 @@ +function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("local_qtracker/question_issue_page",["exports","jquery","core/templates","core/ajax","core/url","core/str","local_qtracker/sidebar","local_qtracker/dropdown","local_qtracker/issue","core/modal_factory","core/modal_events","local_qtracker/dropdown_events","local_qtracker/api_helpers"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=p(b);c=p(c);d=p(d);e=p(e);f=o(f);g=p(g);h=p(h);i=p(i);j=p(j);k=p(k);l=p(l);function n(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;n=function(){return a};return a}function o(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=n();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function p(a){return a&&a.__esModule?a:{default:a}}function q(a){return u(a)||t(a)||s(a)||r()}function r(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function s(a,b){if(!a)return;if("string"==typeof a)return v(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return v(a,b)}function t(a){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(a))return Array.from(a)}function u(a){if(Array.isArray(a))return v(a)}function v(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c").attr("href",c._getIssueUrl(a.id)).html("#"+a.id).prop("outerHTML")}).join(", ");a.t0=this;a.next=9;return f.get_string("issuesuperseded","local_qtracker",e);case 9:a.t1=a.sent;a.t2={message:a.t1,announce:!1,type:"warning"};a.t0.notify.call(a.t0,a.t2,"#qtracker-superseded");case 12:case"end":return a.stop();}}},a,this)}));return function checkForParents(){return a.apply(this,arguments)}}()},{key:"loadSettings",value:function loadSettings(){var a=JSON.parse(sessionStorage.getItem("local_qtracker_issue_page_filter"));if(null!==a){a.forEach(this.filter.add,this.filter)}}},{key:"saveSettings",value:function saveSettings(){var a=Array.from(this.filter);sessionStorage.setItem("local_qtracker_issue_page_filter",JSON.stringify(a))}},{key:"loadChildren",value:function(){var a=x(regeneratorRuntime.mark(function a(){var b,c,d;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:a.next=2;return this.loadIssueChildren(this.issueid);case 2:b=a.sent;c=b.children;d=c.map(function(a){return[a.id,a.title]});return a.abrupt("return",d);case 6:case"end":return a.stop();}}},a,this)}));return function loadChildren(){return a.apply(this,arguments)}}()},{key:"getIssue",value:function(){var a=x(regeneratorRuntime.mark(function a(){return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:if(!(null===this.issue)){a.next=4;break}a.next=3;return i.default.load(this.issueid);case 3:this.issue=a.sent;case 4:return a.abrupt("return",this.issue);case 5:case"end":return a.stop();}}},a,this)}));return function getIssue(){return a.apply(this,arguments)}}()},{key:"registerEditTitleButtonListener",value:function registerEditTitleButtonListener(){(0,b.default)(".edittitle").children("button").on("click",function(){var a=x(regeneratorRuntime.mark(function a(c){var d,e,g,h;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=(0,b.default)(c.target);if(this.editingTitle){a.next=12;break}a.t0=d;a.next=5;return f.get_string("save","core");case 5:a.t1=a.sent;a.t0.html.call(a.t0,a.t1);(0,b.default)(C.TITLE_INPUT).show();(0,b.default)(C.TITLE_TEXT).parent("div").hide();this.editingTitle=!0;a.next=30;break;case 12:e=(0,b.default)(C.TITLE_INPUT).val();a.next=15;return this.getIssue();case 15:g=a.sent;g.setTitle(e);a.next=19;return g.save();case 19:h=a.sent;if(!h.status){a.next=30;break}a.t2=d;a.next=24;return f.get_string("edit","core");case 24:a.t3=a.sent;a.t2.html.call(a.t2,a.t3);(0,b.default)(C.TITLE_INPUT).hide();(0,b.default)(C.TITLE_TEXT).parent("div").show();(0,b.default)(C.TITLE_TEXT).text(e);this.editingTitle=!1;case 30:case"end":return a.stop();}}},a,this)}));return function(){return a.apply(this,arguments)}}().bind(this))}},{key:"initDropdowns",value:function(){var a=x(regeneratorRuntime.mark(function a(){var c,d;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:c=new h.default("#linkedissues-dropdown");a.t0=c;a.next=4;return this.loadChildren();case 4:a.t1=a.sent;a.t0.setItems.call(a.t0,a.t1,!0);this.updateIssueAsideBlock(c.getActiveItems());d=c;d.getRoot().on(l.default.search,function(){var a=x(regeneratorRuntime.mark(function a(b,d){var e,f,g,h,i;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:e=[];if(d.startsWith("#")){f=parseInt(d.substr(1));e.push({key:"id",value:f})}else{if(2 #"+a.id+"";return[a.id,b]});c.setItems(i);c.renderItems();case 9:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}().bind(this));d.getRoot().on(l.default.click,function(){var a=x(regeneratorRuntime.mark(function a(g,e){var h=this,i,l,m,n,o;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:i=parseInt(this.issueid);l=parseInt(e.attr("data-value"));m=d.isActiveItem(l);if(!m){a.next=10;break}a.next=6;return this.deleteIssueRelation(i,l);case 6:n=a.sent;if(n.status){this.renderSidebarContent();c.setItemStatus(l,!m);c.reset();this.updateIssueAsideBlock(c.getActiveItems())}a.next=30;break;case 10:a.t0=j.default;a.next=13;return f.get_string("subsumeissue","local_qtracker");case 13:a.t1=a.sent;a.next=16;return f.get_string("subsumeissueconfirm","local_qtracker",{child:l,parent:i});case 16:a.t2=a.sent;a.t3=j.default.types.SAVE_CANCEL;a.t4={title:a.t1,body:a.t2,type:a.t3,large:!1};a.next=21;return a.t0.create.call(a.t0,a.t4);case 21:o=a.sent;a.t5=o;a.next=25;return f.get_string("confirm","core");case 25:a.t6=a.sent;a.t5.setSaveButtonText.call(a.t5,a.t6);o.getRoot().on(k.default.save,function(){var a=x(regeneratorRuntime.mark(function a(d){var e,g;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d.preventDefault();a.next=3;return h.setIssueRelation(i,l);case 3:e=a.sent;if(!e.status){a.next=11;break}h.renderSidebarContent();c.setItemStatus(l,!m);c.reset();h.updateIssueAsideBlock(c.getActiveItems());a.next=18;break;case 11:g=(0,b.default)("").attr("href",h._getIssueUrl(l)).html("#"+l).prop("outerHTML");a.t0=h;a.next=15;return f.get_string("errorsubsumingissue","local_qtracker",g);case 15:a.t1=a.sent;a.t2={message:a.t1,announce:!0,closebutton:!0,type:"error"};a.t0.notify.call(a.t0,a.t2,"#qtracker-notifications");case 18:o.destroy();case 19:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}());o.getRoot().on(k.default.hidden,function(){o.destroy()});o.show();case 30:case"end":return a.stop();}}},a,this)}));return function(){return a.apply(this,arguments)}}().bind(this));c.renderItems();case 11:case"end":return a.stop();}}},a,this)}));return function initDropdowns(){return a.apply(this,arguments)}}()},{key:"updateIssueAsideBlock",value:function(){var a=x(regeneratorRuntime.mark(function a(c){var d=this,e,g;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:e=[];if(!(0===c.size)){a.next=8;break}a.t0=(0,b.default)("
").addClass("dropdown-item disabled");a.next=5;return f.get_string("noitems","local_qtracker");case 5:a.t1=a.sent;g=a.t0.html.call(a.t0,a.t1).prop("outerHTML");e.push(g);case 8:c.forEach(function(a,c){var f=(0,b.default)("").addClass("list-item border-0 p-1").attr("href",d._getIssueUrl(c)).html(a).prop("outerHTML");e.push(f)});(0,b.default)(".linkedissues-list").html(e);case 10:case"end":return a.stop();}}},a)}));return function updateIssueAsideBlock(){return a.apply(this,arguments)}}()},{key:"_getIssueUrl",value:function _getIssueUrl(a){var b=e.default.relativeUrl("/local/qtracker/issue.php",{courseid:this.courseid,issueid:a});return b}},{key:"renderSidebarContent",value:function(){var a=x(regeneratorRuntime.mark(function a(){var c=this,d,e,f,g,h,i;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=null;this.sidebar.setLoading(!0);this.sidebar.empty();a.next=5;return this.loadIssues(this.questionid,d);case 5:e=a.sent;f=e.issues;g=q(new Set(f.map(function(a){return a.userid})));a.next=10;return this.loadUsersData(g);case 10:h=a.sent;i=[];f.forEach(function(){var a=x(regeneratorRuntime.mark(function a(b){var d;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=h.find(function(a){var c=a.id;return c===b.userid});if(!(b.id==c.issueid)){a.next=3;break}return a.abrupt("return");case 3:i.push(c.addIssueItem(b,d));case 4:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}());self=this;b.default.when.apply(b.default,i).done(function(){self.sidebar.setLoading(!1);b.default.each(arguments,function(a,b){self.sidebar.addTemplateItem(b.html,b.js)});self.applyFilter()}).catch(function(a){console.error(a)});case 15:case"end":return a.stop();}}},a,this)}));return function renderSidebarContent(){return a.apply(this,arguments)}}()},{key:"initSidebar",value:function(){var a=x(regeneratorRuntime.mark(function a(){var c,d,e,f,h,i;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:c=this.filter.has("Closed");d=[{name:"toggleclosed",text:"Show closed issues",value:0,checkbox:!0,active:c}];this.sidebar=new g.default("#question-issues-sidebar",!0,"left",!1,"30%","1.25rem",!1,d);a.next=5;return this.sidebar.render();case 5:this.sidebar.empty();this.sidebar.setLoading(!0);a.next=9;return this.loadQuestionData(this.questionid);case 9:e=a.sent;f=e.question;h=this.getQuestionEditUrl(this.courseid,this.questionid);i=(0,b.default)("").attr("href",h).html(f.name+" #"+f.id);this.sidebar.setTitle(i);this.sidebar.show();a.next=17;return this.renderSidebarContent();case 17:this.sidebar.getContainer().on("click",function(){var a=x(regeneratorRuntime.mark(function a(c){var d,e,f,g,h,i;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=(0,b.default)(c.target);if(!d.hasClass("dropdown-item")){a.next=19;break}e=d.attr("data-name");f=parseInt(d.attr("data-value"));a.t0=e;a.next="toggleclosed"===a.t0?7:"subsume"===a.t0?11:18;break;case 7:if(d.is(":checked")){this.filter.add("Closed");d.prop("checked",!0)}else{this.filter.delete("Closed");d.prop("checked",!1)}this.applyFilter();this.saveSettings();return a.abrupt("break",19);case 11:g=this.issueid;h=f;a.next=15;return this.setIssueRelation(g,h);case 15:i=a.sent;if(i.status){this.renderSidebarContent()}return a.abrupt("break",19);case 18:return a.abrupt("break",19);case 19:case"end":return a.stop();}}},a,this)}));return function(){return a.apply(this,arguments)}}().bind(this));window.closeIssuesPane=function(){this.sidebar.hide()}.bind(this);window.toggleIssuesPane=function(){this.sidebar.togglePane()}.bind(this);case 20:case"end":return a.stop();}}},a,this)}));return function initSidebar(){return a.apply(this,arguments)}}()},{key:"applyFilter",value:function applyFilter(){this.resetFilter();var a=this;this.sidebar.getItems().each(function(){if(!a.filter.has((0,b.default)(this).find(".badge").text())){(0,b.default)(this).hide()}})}},{key:"resetFilter",value:function resetFilter(){this.sidebar.getItems().each(function(){(0,b.default)(this).show()})}},{key:"addIssueItem",value:function(){var a=x(regeneratorRuntime.mark(function a(b,d){var f,g,h,i,j,k=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:f=2.\n\n/**\n * Class for handling question issue page.\n *\n * @module local_qtracker/QuestionIssuePage\n * @class QuestionIssuePage\n * @package local_qtracker\n * @author André Storhaug \n * @copyright 2021 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport $ from 'jquery';\nimport Templates from 'core/templates';\nimport Ajax from 'core/ajax';\nimport url from 'core/url';\nimport * as Str from 'core/str';\nimport Sidebar from 'local_qtracker/sidebar';\nimport Dropdown from 'local_qtracker/dropdown';\nimport Issue from 'local_qtracker/issue';\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\nimport DropdownEvents from 'local_qtracker/dropdown_events';\nimport { loadIssuesData } from 'local_qtracker/api_helpers';\n\nvar SELECTORS = {\n TITLE: '[data-region=\"issuetitle\"]',\n TITLE_TEXT: '[data-region=\"issuetitle-text\"]',\n TITLE_INPUT: '[data-region=\"issuetitle-input\"]',\n};\n\n/**\n * Constructor\n * @constructor\n * @param {String} courseid\n * @param {int} questionid\n * @param {int} issueid\n *\n * Each call gets it's own instance of this class.\n */\nclass QuestionIssuePage {\n courseid = null;\n questionid = null;\n issueid = null;\n issue = null;\n parents = [];\n filter = new Set(['Open', 'New'])\n\n constructor(courseid, questionid, issueid) {\n this.courseid = courseid;\n this.questionid = questionid;\n this.issueid = parseInt(issueid);\n this.issue = null;\n this.loadSettings()\n\n this.editingTitle = false;\n this.init();\n }\n\n async init() {\n this.checkForParents()\n this.initSidebar()\n this.initDropdowns();\n this.registerEditTitleButtonListener()\n }\n\n async checkForParents() {\n\n let parentsData = await this.loadIssueParents(this.issueid);\n if (parentsData.parents.length > 0) {\n this.parents = parentsData.parents;\n let supersededids = this.parents.map((parent) => {\n return $('')\n .attr(\"href\", this._getIssueUrl(parent.id))\n .html(\"#\" + parent.id).prop('outerHTML');\n }).join(\", \");\n this.notify({\n message: await Str.get_string('issuesuperseded', 'local_qtracker', supersededids),\n announce: false,\n type: \"warning\",\n }, '#qtracker-superseded');\n }\n }\n\n loadSettings() {\n let filterData = JSON.parse(sessionStorage.getItem('local_qtracker_issue_page_filter'))\n if (filterData !== null) {\n filterData.forEach(this.filter.add, this.filter)\n }\n }\n\n saveSettings() {\n let filterData = Array.from(this.filter);\n sessionStorage.setItem('local_qtracker_issue_page_filter', JSON.stringify(filterData))\n }\n\n\n async loadChildren() {\n let childrenData = await this.loadIssueChildren(this.issueid);\n let children = childrenData.children;\n let items = children.map((item) => [item.id, item.title]);\n return items;\n }\n\n async getIssue() {\n if (this.issue === null) {\n this.issue = await Issue.load(this.issueid);\n }\n return this.issue;\n }\n\n registerEditTitleButtonListener() {\n $(\".edittitle\").children(\"button\").on('click', async function (e) {\n let button = $(e.target);\n if (!this.editingTitle) {\n button.html(await Str.get_string('save', 'core'));\n $(SELECTORS.TITLE_INPUT).show();\n $(SELECTORS.TITLE_TEXT).parent(\"div\").hide();\n this.editingTitle = true;\n } else {\n let title = $(SELECTORS.TITLE_INPUT).val();\n let issue = await this.getIssue();\n issue.setTitle(title);\n let response = await issue.save();\n if (response.status) {\n button.html(await Str.get_string('edit', 'core'));\n $(SELECTORS.TITLE_INPUT).hide();\n $(SELECTORS.TITLE_TEXT).parent(\"div\").show();\n $(SELECTORS.TITLE_TEXT).text(title);\n this.editingTitle = false;\n }\n }\n }.bind(this));\n }\n\n // aside blocks\n async initDropdowns() {\n let issuesDropdown = new Dropdown('#linkedissues-dropdown');\n issuesDropdown.setItems(await this.loadChildren(), true);\n this.updateIssueAsideBlock(issuesDropdown.getActiveItems());\n\n // TODO: Remove the right margin of the content container when the grading panel is hidden so that it expands to full-width.\n let dropdown = issuesDropdown;\n dropdown.getRoot().on(DropdownEvents.search, async function (e, str) {\n let criteria = [];\n if (str.startsWith('#')) {\n let id = parseInt(str.substr(1));\n criteria.push({ key: 'id', value: id });\n } else {\n if (str.length > 2) str += \"%\"\n criteria.push({ key: 'title', value: str });\n }\n let issuesResponse = await loadIssuesData(criteria);\n let issues = issuesResponse.issues;\n let items = issues.map((item) => {\n let html = item.title + ' #' + item.id + '';\n return [item.id, html];\n });\n issuesDropdown.setItems(items);\n issuesDropdown.renderItems();\n }.bind(this));\n\n\n dropdown.getRoot().on(DropdownEvents.click, async function (e, element) {\n let parentid = parseInt(this.issueid);\n let childid = parseInt(element.attr(\"data-value\"));\n let active = dropdown.isActiveItem(childid);\n if (active) {\n let response = await this.deleteIssueRelation(parentid, childid);\n if (response.status) {\n this.renderSidebarContent();//TODO: update sidebar issues if exists with new status.\n issuesDropdown.setItemStatus(childid, !active)\n issuesDropdown.reset();\n this.updateIssueAsideBlock(issuesDropdown.getActiveItems())\n }\n } else {\n let modal = await ModalFactory.create({\n title: await Str.get_string('subsumeissue', 'local_qtracker'),\n body: await Str.get_string('subsumeissueconfirm', 'local_qtracker', { child: childid, parent: parentid }),\n type: ModalFactory.types.SAVE_CANCEL,\n large: false,\n })\n modal.setSaveButtonText(await Str.get_string('confirm', 'core'));\n modal.getRoot().on(ModalEvents.save, async e => {\n // Don't close the modal yet.\n e.preventDefault();\n // Submit form data.\n let response = await this.setIssueRelation(parentid, childid);\n if (response.status) {\n this.renderSidebarContent();//TODO: update sidebar issues if exists with new status.\n issuesDropdown.setItemStatus(childid, !active)\n issuesDropdown.reset();\n this.updateIssueAsideBlock(issuesDropdown.getActiveItems())\n } else {\n let issueurl = $('')\n .attr(\"href\", this._getIssueUrl(childid))\n .html(\"#\" + childid).prop('outerHTML');\n\n this.notify({\n message: await Str.get_string('errorsubsumingissue', 'local_qtracker', issueurl),\n announce: true,\n closebutton: true,\n type: \"error\",\n }, '#qtracker-notifications');\n }\n modal.destroy();\n\n //submitEditFormAjax(link, getBody, modal, userEnrolmentId, container.dataset);\n });\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Destroy when hidden.\n modal.destroy();\n });\n // Show the modal.\n modal.show();\n }\n }.bind(this))\n\n issuesDropdown.renderItems();\n }\n\n async updateIssueAsideBlock(items) {\n let elements = []\n\n if (items.size === 0) {\n let element = $('
')\n .addClass(\"dropdown-item disabled\")\n .html(await Str.get_string('noitems', 'local_qtracker'))\n .prop('outerHTML');\n elements.push(element)\n }\n\n items.forEach((html, key) => {\n let element = $('')\n .addClass(\"list-item border-0 p-1\")\n .attr(\"href\", this._getIssueUrl(key))\n .html(html).prop('outerHTML');\n //
{{{text}}}
\n elements.push(element);\n });\n $(\".linkedissues-list\").html(elements);\n }\n\n _getIssueUrl(issueid) {\n let issueurl = url.relativeUrl('/local/qtracker/issue.php', {\n courseid: this.courseid,\n issueid: issueid,\n });\n return issueurl;\n }\n\n async renderSidebarContent() {\n let state = null;\n this.sidebar.setLoading(true);\n this.sidebar.empty();\n\n // Get issues data.\n let issuesResponse = await this.loadIssues(this.questionid, state);\n let issues = issuesResponse.issues;\n\n // Get users data.\n let userids = [...new Set(issues.map(issue => issue.userid))];\n let usersData = await this.loadUsersData(userids);\n\n // Render issue items.\n let promises = [];\n issues.forEach(async issueData => {\n let userData = usersData.find(({ id }) => id === issueData.userid);\n if (issueData.id == this.issueid) {\n return;\n }\n promises.push(this.addIssueItem(issueData, userData));\n });\n\n self = this;\n // When all issue item promises are resolved.\n $.when.apply($, promises).done(function () {\n self.sidebar.setLoading(false);\n $.each(arguments, (index, argument) => {\n self.sidebar.addTemplateItem(argument.html, argument.js);\n });\n self.applyFilter();\n }).catch(e => {\n console.error(e);\n });\n }\n\n async initSidebar() {\n let active = this.filter.has(\"Closed\");\n let sidebarOptions = [{ \"name\": \"toggleclosed\", \"text\": \"Show closed issues\", \"value\": 0, \"checkbox\": true, \"active\": active }];\n this.sidebar = new Sidebar('#question-issues-sidebar', true, \"left\", false, '30%', '1.25rem', false, sidebarOptions);\n\n await this.sidebar.render();\n\n this.sidebar.empty();\n this.sidebar.setLoading(true);\n\n // Get question title.\n let questionData = await this.loadQuestionData(this.questionid);\n let question = questionData.question;\n let questionEditUrl = this.getQuestionEditUrl(this.courseid, this.questionid);\n let link = $('').attr(\"href\", questionEditUrl).html(question.name + \" #\" + question.id);\n this.sidebar.setTitle(link);\n this.sidebar.show();\n\n await this.renderSidebarContent();\n\n // Add logic to sidebar actions (dropdowns)\n this.sidebar.getContainer().on('click', async function (e) {\n let element = $(e.target);\n if (element.hasClass(\"dropdown-item\")) {\n let dropdownItem = element.attr(\"data-name\");\n let itemValue = parseInt(element.attr(\"data-value\"));\n switch (dropdownItem) {\n case \"toggleclosed\": // Sidebar toolbar filter\n if (element.is(':checked')) {\n this.filter.add('Closed');\n element.prop('checked', true);\n } else {\n this.filter.delete('Closed');\n element.prop('checked', false);\n }\n this.applyFilter();\n this.saveSettings();\n break;\n case \"subsume\": // Sidebar item action\n //TODO: add issue menu to the sidebar items\n let parentid = this.issueid;\n let childid = itemValue;\n let response = await this.setIssueRelation(parentid, childid);\n if (response.status) {\n this.renderSidebarContent()\n }\n break;\n default:\n break;\n }\n }\n }.bind(this))\n\n window.closeIssuesPane = function () { this.sidebar.hide() }.bind(this);\n window.toggleIssuesPane = function () { this.sidebar.togglePane() }.bind(this);\n }\n\n applyFilter() {\n this.resetFilter();\n let self = this;\n this.sidebar.getItems().each(function () {\n if (!self.filter.has($(this).find(\".badge\").text())) {\n $(this).hide();\n }\n });\n }\n\n resetFilter() {\n this.sidebar.getItems().each(function () {\n $(this).show();\n });\n }\n\n /**\n *\n * @param {object} issueData\n * @param {object} userData\n * @return {Promise}\n */\n async addIssueItem(issueData, userData, extraClasses = \"\") {\n // Fetch user data.\n let issueurl = url.relativeUrl('/local/qtracker/issue.php', {\n courseid: this.courseid,\n issueid: issueData.id,\n });\n let userurl = url.relativeUrl('/user/view.php', {\n course: this.courseid,\n id: userData.id,\n });\n\n let actions = {\n \"trigger\": {\n \"key\": \"fa-ellipsis-h\",\n \"title\": \"Options\",\n \"alt\": \"Show options\",\n \"extraclasses\": \"\",\n \"unmappedIcon\": false\n },\n \"header\": false,\n \"items\": [\n { \"name\": \"subsume\", \"text\": \"Subsume\", \"value\": issueData.id },\n ]\n }\n // Render issues pane\n let paneContext = {\n issueurl: issueurl,\n userurl: userurl,\n profileimageurl: userData.profileimageurlsmall,\n fullname: userData.fullname,\n timecreated: issueData.timecreated,\n id: issueData.id,\n title: issueData.title,\n description: issueData.description,\n extraclasses: extraClasses\n //actions: actions // TODO: finish this\n };\n let state = issueData.state;\n paneContext[state] = true;\n\n return Templates.render('local_qtracker/sidebar_item_issue', paneContext)\n .then(function (html, js) {\n return { html: html, js: js };\n });\n }\n\n async loadIssues(id, state = null) {\n let criteria = [\n { key: 'questionid', value: id },\n ];\n if (state) {\n criteria.push({ key: 'state', value: state });\n }\n let issuesData = await Ajax.call([{\n methodname: 'local_qtracker_get_issues',\n args: { criteria: criteria }\n }])[0];\n\n return issuesData;\n }\n\n async loadUsersData(ids) {\n let usersData = await Ajax.call([{\n methodname: 'core_user_get_users_by_field',\n args: {\n field: 'id',\n values: ids\n }\n }])[0];\n return usersData;\n }\n\n async setIssueRelation(parentid, childid) {\n let result = await Ajax.call([{\n methodname: 'local_qtracker_set_issue_relation',\n args: {\n parentid: parentid,\n childid: childid,\n }\n }])[0];\n return result\n }\n\n\n async deleteIssueRelation(parentid, childid) {\n let result = await Ajax.call([{\n methodname: 'local_qtracker_delete_issue_relation',\n args: {\n parentid: parentid,\n childid: childid,\n }\n }])[0];\n return result\n }\n\n getQuestionEditUrl(courseid, questionid) {\n let returnurl = encodeURIComponent(location.pathname + location.search);\n let editurl = url.relativeUrl('/question/question.php', {\n courseid: courseid,\n id: questionid,\n returnurl: returnurl,\n });\n return editurl;\n }\n\n decodeHTML(html) {\n var doc = new DOMParser().parseFromString(html, \"text/html\");\n return doc.documentElement.textContent;\n }\n\n async loadQuestionData(id) {\n let userData = await Ajax.call([{\n methodname: 'local_qtracker_get_question',\n args: {\n id: id\n }\n }])[0];\n return userData;\n }\n\n async loadIssueParents(id) {\n let userData;\n userData = await Ajax.call([{\n methodname: 'local_qtracker_get_issue_parents',\n args: {\n issueid: id\n }\n }])[0];\n return userData;\n }\n\n async loadIssueChildren(id) {\n let userData;\n userData = await Ajax.call([{\n methodname: 'local_qtracker_get_issue_children',\n args: {\n issueid: id\n }\n }])[0];\n return userData;\n }\n\n notify(notification, selector = null) {\n notification = $.extend({\n closebutton: false,\n announce: false,\n type: 'error',\n extraclasses: \"show\",\n }, notification);\n\n let types = {\n 'success': 'core/notification_success',\n 'info': 'core/notification_info',\n 'warning': 'core/notification_warning',\n 'error': 'core/notification_error',\n };\n\n let template = types[notification.type];\n Templates.render(template, notification)\n .then((html, js) => {\n if (selector === null) {\n $('#qtracker-notifications').append(html);\n } else {\n $(selector).append(html);\n }\n Templates.runTemplateJS(js);\n })\n .catch((error) => {\n console.error(error);\n throw error;\n });\n };\n}\n\nexport default QuestionIssuePage;\n"],"file":"question_issue_page.min.js"} \ No newline at end of file diff --git a/amd/build/questions_table_page.min.js b/amd/build/questions_table_page.min.js new file mode 100644 index 0000000..48087da --- /dev/null +++ b/amd/build/questions_table_page.min.js @@ -0,0 +1,2 @@ +define ("local_qtracker/questions_table_page",["exports","jquery","core/templates","core/ajax","core/url","local_qtracker/sidebar"],function(a,b,c,d,e,f){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=g(b);c=g(c);d=g(d);e=g(e);f=g(f);function g(a){return a&&a.__esModule?a:{default:a}}function h(a){return l(a)||k(a)||j(a)||i()}function i(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function j(a,b){if(!a)return;if("string"==typeof a)return m(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return m(a,b)}function k(a){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(a))return Array.from(a)}function l(a){if(Array.isArray(a))return m(a)}function m(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c").attr("href",i).html(g.name+" #"+g.id);this.sidebar.setTitle(j);this.sidebar.show();a.next=13;return this.loadIssues(c,e);case 13:k=a.sent;l=k.issues;m=h(new Set(l.map(function(a){return a.userid})));a.next=18;return this.loadUsersData(m);case 18:n=a.sent;p=[];l.forEach(function(){var a=o(regeneratorRuntime.mark(function a(b){var c;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:c=n.find(function(a){var c=a.id;return c===b.userid});p.push(d.addIssueItem(b,c));case 2:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}());self=this;b.default.when.apply(b.default,p).done(function(){self.sidebar.setLoading(!1);b.default.each(arguments,function(a,b){self.sidebar.addTemplateItem(b.html,b.js)})}).catch(function(a){console.error(a)});case 23:case"end":return a.stop();}}},a,this)}));return function(){return a.apply(this,arguments)}}().bind(this);window.closeIssuesPane=function(){this.sidebar.hide()}.bind(this);window.toggleIssuesPane=function(){this.sidebar.togglePane()}.bind(this);case 5:case"end":return a.stop();}}},a,this)}));return function init(){return a.apply(this,arguments)}}()},{key:"addIssueItem",value:function(){var a=o(regeneratorRuntime.mark(function a(b,d){var f,g,h,i;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:f=e.default.relativeUrl("/local/qtracker/issue.php",{courseid:this.courseid,issueid:b.id});g=e.default.relativeUrl("/user/view.php",{course:this.courseid,id:d.id});h={issueurl:f,userurl:g,profileimageurl:d.profileimageurlsmall,fullname:d.fullname,timecreated:b.timecreated,id:b.id,title:b.title,description:b.description};i=b.state;h[i]=!0;return a.abrupt("return",c.default.render("local_qtracker/sidebar_item_issue",h).then(function(a,b){return{html:a,js:b}}));case 6:case"end":return a.stop();}}},a,this)}));return function addIssueItem(){return a.apply(this,arguments)}}()},{key:"loadIssues",value:function(){var a=o(regeneratorRuntime.mark(function a(b){var c,e,f,g=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:c=1.\n\n/**\n * Class for handling page with table of questions with issues.\n *\n * @module local_qtracker/QuestionsTablePage\n * @class QuestionsTablePage\n * @package local_qtracker\n * @author André Storhaug \n * @copyright 2021 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport $ from 'jquery';\nimport Templates from 'core/templates';\nimport Ajax from 'core/ajax';\nimport url from 'core/url';\nimport Sidebar from 'local_qtracker/sidebar';\n\n/**\n * Constructor\n * @constructor\n * @param {String} selector used to find triggers for the new group modal.\n * @param {int} contextid\n *\n * Each call to init gets it's own instance of this class.\n */\nclass QuestionsTablePage {\n courseid = null;\n\n constructor(courseid) {\n this.courseid = courseid;\n this.sidebar = new Sidebar('#questions-table-sidebar', false, \"right\", false, '40%');\n this.init();\n }\n\n async init() {\n\n await this.sidebar.render();\n window.showIssuesInPane = async function (id, state = null) {\n this.sidebar.empty();\n this.sidebar.setLoading(true);\n\n // Get question title.\n let questionData = await this.loadQuestionData(id);\n let question = questionData.question;\n let questionEditUrl = this.getQuestionEditUrl(this.courseid, id);\n let link = $('').attr(\"href\", questionEditUrl).html(question.name + \" #\" + question.id);\n this.sidebar.setTitle(link);\n this.sidebar.show();\n\n // Get issues data.\n let issuesResponse = await this.loadIssues(id, state);\n let issues = issuesResponse.issues;\n\n // Get users data.\n let userids = [...new Set(issues.map(issue => issue.userid))];\n let usersData = await this.loadUsersData(userids);\n\n // Render issue items.\n let promises = [];\n issues.forEach(async issueData => {\n let userData = usersData.find(({ id }) => id === issueData.userid);\n promises.push(this.addIssueItem(issueData, userData));\n });\n\n self = this;\n // When all issue item promises are resolved.\n $.when.apply($, promises).done(function () {\n self.sidebar.setLoading(false);\n $.each(arguments, (index, argument) => {\n self.sidebar.addTemplateItem(argument.html, argument.js);\n });\n }).catch(e => {\n console.error(e);\n });\n\n }.bind(this);\n\n window.closeIssuesPane = function () { this.sidebar.hide() }.bind(this);\n window.toggleIssuesPane = function () { this.sidebar.togglePane() }.bind(this);\n\n }\n\n /**\n *\n * @param {object} issueData\n * @param {object} userData\n * @return {Promise}\n */\n async addIssueItem(issueData, userData) {\n // Fetch user data.\n let issueurl = url.relativeUrl('/local/qtracker/issue.php', {\n courseid: this.courseid,\n issueid: issueData.id,\n });\n let userurl = url.relativeUrl('/user/view.php', {\n course: this.courseid,\n id: userData.id,\n });\n\n // Render issues pane\n let paneContext = {\n issueurl: issueurl,\n userurl: userurl,\n profileimageurl: userData.profileimageurlsmall,\n fullname: userData.fullname,\n timecreated: issueData.timecreated,\n id: issueData.id,\n title: issueData.title,\n description: issueData.description,\n };\n let state = issueData.state;\n paneContext[state] = true;\n\n return Templates.render('local_qtracker/sidebar_item_issue', paneContext)\n .then(function (html, js) {\n return { html: html, js: js };\n });\n }\n\n async loadIssues(id, state = null) {\n let criteria = [\n { key: 'questionid', value: id },\n ];\n if (state) {\n criteria.push({ key: 'state', value: state });\n }\n let issuesData = await Ajax.call([{\n methodname: 'local_qtracker_get_issues',\n args: { criteria: criteria }\n }])[0];\n\n return issuesData;\n }\n\n async loadUsersData(ids) {\n let usersData = await Ajax.call([{\n methodname: 'core_user_get_users_by_field',\n args: {\n field: 'id',\n values: ids\n }\n }])[0];\n return usersData;\n }\n\n getQuestionEditUrl(courseid, questionid) {\n let returnurl = encodeURIComponent(location.pathname + location.search);\n let editurl = url.relativeUrl('/question/question.php', {\n courseid: courseid,\n id: questionid,\n returnurl: returnurl,\n });\n return editurl;\n }\n\n decodeHTML(html) {\n var doc = new DOMParser().parseFromString(html, \"text/html\");\n return doc.documentElement.textContent;\n }\n\n async loadQuestionData(id) {\n let userData = await Ajax.call([{\n methodname: 'local_qtracker_get_question',\n args: {\n id: id\n }\n }])[0];\n return userData;\n }\n}\n\nexport default QuestionsTablePage;\n"],"file":"questions_table_page.min.js"} \ No newline at end of file diff --git a/amd/build/sidebar.min.js b/amd/build/sidebar.min.js new file mode 100644 index 0000000..b556e23 --- /dev/null +++ b/amd/build/sidebar.min.js @@ -0,0 +1,2 @@ +define ("local_qtracker/sidebar",["exports","jquery","core/templates"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}function e(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function f(a){return function(){var b=this,c=arguments;return new Promise(function(d,f){var i=a.apply(b,c);function g(a){e(i,d,f,g,h,"next",a)}function h(a){e(i,d,f,g,h,"throw",a)}g(void 0)})}}function g(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function h(a,b){for(var c=0,d;c.\n\n/**\n * Manager for managing a sidebar.\n *\n * @module local_qtracker/Sidebar\n * @class Sidebar\n * @package local_qtracker\n * @author André Storhaug \n * @copyright 2021 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport $ from 'jquery';\nimport Templates from 'core/templates';\n\n/**\n * Constructor\n * @constructor\n * @param {String} selector used to find triggers for the new group modal.\n * @param {int} contextid\n *\n * Each call to init gets it's own instance of this class.\n */\nclass Sidebar {\n hidden = null;\n loading = null;\n width = null;\n margin = null;\n container = null;\n closable = null;\n options = null;\n\n constructor(container, show = false, side = 'right', loading = false, width = '40%', margin = '0px', closable = true, options = []) {\n this.container = $(container); // Container element\n this.hidden = !show;\n this.visible = show;\n this.side = side;\n this.loading = loading;\n this.width = width;\n this.margin = margin;\n\n this.closable = closable;\n this.options = options;\n\n this.mql = window.matchMedia('(min-width: 768px)');\n this.mql.addEventListener('change', this.screenTest.bind(this));\n\n this.render = this.render.bind(this);\n }\n\n screenTest(e) {\n if (!this.visible) {\n return;\n }\n if (e.matches) {\n /* the viewport is 768px pixels wide or more */\n $('#qtracker-sidebar').css('width', 'calc(' + this.width + ' - -1.25rem)');\n $('.qtracker-push-pane-over').css('padding-' + this.getSide(), 'calc(' + this.width + ' - -' + this.margin + ')');\n } else {\n /* the viewport is more than 768px pixels wide or less */\n $('#qtracker-sidebar').css('width', '100%');\n $('.qtracker-push-pane-over').css('padding-' + this.getSide(), '0');\n }\n }\n\n async render() {\n let self = this;\n\n let context = {};\n if (this.closable) {\n context.close = {\n \"key\": \"fa-times\",\n \"title\": \"Close\",\n \"alt\": \"Close pane\",\n \"extraclasses\": \"\",\n \"unmappedIcon\": false\n };\n }\n if (this.options.length > 0) {\n context.options = {\n \"trigger\": {\n \"key\": \"fa-filter\",\n \"title\": \"Options\",\n \"alt\": \"Show options\",\n \"extraclasses\": \"\",\n \"unmappedIcon\": false\n },\n \"header\": false,\n \"items\": self.options,\n };\n }\n\n //let self = this;\n await Templates.render('local_qtracker/sidebar', context).then((html, js) => {\n Templates.replaceNodeContents(this.container, html, js);\n this.setVisibility(!this.hidden);\n this.setLoading(this.loading);\n this.setSide(this.side);\n this.screenTest(this.mql)\n });\n }\n\n getSide() {\n return this.side;\n }\n\n getOppositeSide() {\n return this.side == 'left' ? 'right' : 'left';\n }\n\n /**\n *\n * @param {*} width The sidebar width\n * @param {*} width2 The existing content width\n */\n setWidth(width, width2) {\n this.width = width;\n this.width2 = width2;\n\n this.screenTest(this.mql);\n }\n\n isMobileWidth() {\n return !this.mql.matches;\n }\n setSide(side) {\n if (side == 'right') {\n $('#qtracker-sidebar').addClass('qtracker-sidebar-right');\n $('#qtracker-sidebar').removeClass('qtracker-sidebar-left');\n } else if (side == 'left') {\n $('#qtracker-sidebar').addClass('qtracker-sidebar-left');\n $('#qtracker-sidebar').removeClass('qtracker-sidebar-right');\n }\n }\n\n setTitle(html) {\n $('.qtracker-sidebar-title').html(html);\n }\n\n setLoading(show = true) {\n if (show) {\n $('.qtracker-sidebar-content .loading').addClass(\"show\");\n this.loading = true;\n } else {\n $('.qtracker-sidebar-content .loading').removeClass(\"show\");\n this.loading = false;\n }\n }\n\n empty() {\n $('.qtracker-sidebar-content .qtracker-items').empty();\n }\n\n addTemplateItem(html, js) {\n Templates.appendNodeContents('.qtracker-sidebar-content .qtracker-items', html, js);\n }\n\n getItems() {\n return $('.qtracker-sidebar-content .qtracker-items').children();\n }\n\n getContainer() {\n return this.container;\n }\n\n hide() {\n if (!this.hidden) {\n this.setVisibility(false);\n }\n }\n\n show() {\n if (this.hidden) {\n this.setVisibility(true);\n }\n }\n\n setVisibility(show = true) {\n if (show) {\n $('.qtracker-push-pane-over').css('padding-' + this.getSide(), 'calc(' + this.width + ' - -' + this.margin + ')');\n $('#qtracker-sidebar').addClass('show');\n this.screenTest(this.mql);\n } else {\n $('.qtracker-push-pane-over').css('padding-' + this.getSide(), '0');\n $('#qtracker-sidebar').removeClass('show');\n }\n this.hidden = !show;\n this.visible = show;\n }\n\n togglePane() {\n $('.qtracker-container').toggleClass('qtracker-push-pane-over');\n $('#qtracker-sidebar').toggleClass(\"show\");\n this.hidden = !this.hidden;\n }\n\n decodeHTML(html) {\n var doc = new DOMParser().parseFromString(html, \"text/html\");\n return doc.documentElement.textContent;\n }\n}\n\nexport default Sidebar;\n"],"file":"sidebar.min.js"} \ No newline at end of file diff --git a/amd/src/api_helpers.js b/amd/src/api_helpers.js new file mode 100644 index 0000000..6868942 --- /dev/null +++ b/amd/src/api_helpers.js @@ -0,0 +1,46 @@ + +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * @module local_qtracker/api_helpers + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from "core/ajax"; + +/** + * TODO: dynamically load "from-to" and use limit. + * @param {*} criteria + * @param {*} from + * @param {*} limit + * @returns + */ +export const loadIssuesData = async (criteria, from = 0, limit = 100) => { + let issuesData = await Ajax.call([{ + methodname: 'local_qtracker_get_issues', + args: { + criteria: criteria, + from: from, + limit: limit, + } + }])[0]; + + return issuesData; +} + diff --git a/amd/src/block_form_manager.js b/amd/src/block_form_manager.js new file mode 100644 index 0000000..bfd86d3 --- /dev/null +++ b/amd/src/block_form_manager.js @@ -0,0 +1,494 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Manager for a Question Tracker Block form. + * + * @module local_qtracker/BlockFormManager + * @class BlockFormManager + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/str', 'core/templates', 'core/ajax', 'local_qtracker/issue', 'local_qtracker/issue_manager'], + function ($, Str, Templates, Ajax, Issue, IssueManager) { + var SELECTORS = { + SLOT: '[name="slot"]', + SLOT_SELECT_OPTION: '[name="slot"] option', + TITLE: '[name="issuetitle"]', + DESCRIPTION: '[name="issuedescription"]', + SUBMIT_BUTTON: 'button[type="submit"]', + DELETE_BUTTON: '#qtracker-delete', + }; + + let VALIDATION_ELEMENTS = [ + SELECTORS.TITLE, + SELECTORS.DESCRIPTION, + ]; + + var NOTIFICATION_DURATION = 7500; + var notificationTimeoutHandle = null; + + /** + * Constructor + * + * @param {String} selector used to find triggers for the new group modal. + * @param {string} issueids + * @param {int} contextid + * + * Each call to init gets it's own instance of this class. + */ + var BlockFormManager = function (selector, issueids, contextid) { + this.contextid = contextid; + this.form = $(selector); + this.form.closest('.card-text').prepend(''); + this.issueManager = new IssueManager(); + this.init(JSON.parse(issueids)); + }; + + /** + * @var {Form} form + * @private + */ + BlockFormManager.prototype.form = null; + + /** + * @var {int} contextid + * @private + */ + BlockFormManager.prototype.contextid = -1; + + /** + * @var {int} issueid + * @private + */ + BlockFormManager.prototype.issueid = null; + + /** + * @var {issue[]} issues + * @private + */ + BlockFormManager.prototype.issues = []; + + /** + * @var {issue_manager} issueManager + * @private + */ + BlockFormManager.prototype.issueManager = null; + + /** + * Initialise the class. + * + * @param {*[]} issueids selector used to find triggers for the new question issue. + * @private + */ + BlockFormManager.prototype.init = function (issueids = []) { + // Init all slots + let slots = $(SELECTORS.SLOT_SELECT_OPTION); + if (slots.length == 0) { + slots = $(SELECTORS.SLOT); + } + slots.map((index, option) => { + let issue = new Issue(null, parseInt(option.value), this.contextid); + issue.isSaved = false;// ChangeState(Issue.STATES.NEW); + this.issueManager.addIssue(issue); + }); + + + this.issueManager.loadIssues(issueids).then(() => { + + var formData = new FormData(this.form[0]); + this.issueManager.setActiveIssue(parseInt(formData.get('slot'))); + + this.reflectFormState(); + + // Issue title event listener. + let titleElement = this.form.find(SELECTORS.TITLE); + titleElement.change((event) => { + this.issueManager.getActiveIssue().setTitle(event.target.value); + }); + /* $(document).on(qtrackerEvents.CHANGED_SLOT_BLOCK_FORM, (event, value) => { + titleElement.val(value); + }); */ + + // Issue description event listener. + let descriptionElement = this.form.find(SELECTORS.DESCRIPTION); + descriptionElement.change((event) => { + this.issueManager.getActiveIssue().setDescription(event.target.value); + }); + /* $(document).on(qtrackerEvents.CHANGED_SLOT_BLOCK_FORM, (event, value) => { + descriptionElement.val(value) + }); */ + + // + + // Load existing issues. + var slotElement = this.form.find(SELECTORS.SLOT); + slotElement.change(this.handleSlotChange.bind(this)); + + this.form.on('submit', this.submitFormAjax.bind(this)); + + }).catch((error) => { + console.error(error); + }); + }; + + BlockFormManager.prototype.handleSlotChange = function (e) { + this.issueManager.setActiveIssue(parseInt(e.target.value)); + this.reflectFormState(); + this.resetValidation(); + }; + + BlockFormManager.prototype.reflectFormState = function () { + let issue = this.issueManager.getActiveIssue(); + if (issue.isSaved === true) { // State === Issue.STATES.EXISTING) { + this.toggleDeleteButton(true); + this.toggleUpdateButton(true); + } else if (issue.isSaved === false) { // State === Issue.STATES.NEW) { + this.clearForm(); + } + + this.restoreForm(); + }; + + /** + * @method handleFormSubmissionResponse + * @param response + * @private + */ + BlockFormManager.prototype.handleFormSubmissionResponse = function (response) { + + // TODO: handle response.status === false + // TODO: handle response.warning ... + + // We could trigger an event instead. + // Yuk. + Y.use('moodle-core-formchangechecker', function () { + M.core_formchangechecker.reset_form_dirty_state(); + }); + // Document.location.reload(); + + this.issueManager.getActiveIssue().setId(response.issueid); + }; + + /** + * @method handleFormSubmissionFailure + * @param response + * @private + */ + BlockFormManager.prototype.handleFormSubmissionFailure = function (response) { + // Oh noes! Epic fail :( + // Ah wait - this is normal. We need to re-display the form with errors! + console.error("An error occured"); + console.error(response); + }; + + BlockFormManager.prototype.clearForm = function () { + // Remove delete button. + this.form.find('#qtracker-delete').remove(); + this.resetValidation(); + Str.get_string('submitnewissue', 'local_qtracker').then(function (string) { + this.form.find('button[type="submit"]').html(string); + }.bind(this)); + }; + + BlockFormManager.prototype.restoreForm = function () { + let issue = this.issueManager.getActiveIssue(); + this.form.find('[name="issuetitle"]').val(issue.getTitle()); + this.form.find('[name="issuedescription"]').val(issue.getDescription()); + + }; + + /** + * @method editIssue + * @private + */ + BlockFormManager.prototype.editIssue = function () { + var formData = new FormData(this.form[0]); + Ajax.call([{ + methodname: 'local_qtracker_edit_issue', + args: { + issueid: this.issueManager.getActiveIssue().getId(), + issuetitle: formData.get('issuetitle'), + issuedescription: formData.get('issuedescription'), + }, + done: function (response) { + Str.get_string('issueupdated', 'local_qtracker').then(function (string) { + let notification = { + message: string, + announce: true, + type: "success", + }; + this.notify(notification); + }.bind(this)); + this.handleFormSubmissionResponse(response); + }.bind(this), + fail: this.handleFormSubmissionFailure.bind(this) + }]); + }; + + /** + * @method editIssue + * @private + */ + BlockFormManager.prototype.deleteIssue = function () { + Ajax.call([{ + methodname: 'local_qtracker_delete_issue', + args: { + issueid: this.issueManager.getActiveIssue().getId(), + }, + done: function () { + Str.get_string('issuedeleted', 'local_qtracker').then(function (string) { + let notification = { + message: string, + announce: true, + type: "success", + }; + this.notify(notification); + }.bind(this)); + this.issueManager.getActiveIssue().isSaved = false;// ChangeState(Issue.STATES.NEW);; + this.clearForm(); + }.bind(this), + fail: this.handleFormSubmissionFailure.bind(this) + }]); + }; + + /** + * @method handleFormSubmissionFailure + * @private + */ + BlockFormManager.prototype.createIssue = function () { + var formData = new FormData(this.form[0]); + // Now we can continue... + Ajax.call([{ + methodname: 'local_qtracker_new_issue', + args: { + qubaid: formData.get('qubaid'), + slot: formData.get('slot'), + contextid: this.contextid, + issuetitle: formData.get('issuetitle'), + issuedescription: formData.get('issuedescription'), + }, + done: function (response) { + Str.get_string('issuecreated', 'local_qtracker').then(function (string) { + let notification = { + message: string, + announce: true, + type: "success", + }; + this.notify(notification); + }.bind(this)); + this.issueManager.getActiveIssue().isSaved = true;// ChangeState(Issue.STATES.EXISTING) + // This.setAction(ACTION.EDITISSUE); + // TODO: add delete button. + this.toggleUpdateButton(true); + this.toggleDeleteButton(true); + + this.handleFormSubmissionResponse(response); + }.bind(this), + fail: this.handleFormSubmissionFailure.bind(this) + }]); + }; + + /** + * Cancel any typing pause timer. + */ + BlockFormManager.prototype.cancelNotificationTimer = function () { + if (notificationTimeoutHandle) { + clearTimeout(notificationTimeoutHandle); + } + notificationTimeoutHandle = null; + }; + + BlockFormManager.prototype.notify = function (notification) { + notification = $.extend({ + closebutton: true, + announce: true, + type: 'error', + extraclasses: "show", + }, notification); + + let types = { + 'success': 'core/notification_success', + 'info': 'core/notification_info', + 'warning': 'core/notification_warning', + 'error': 'core/notification_error', + }; + + this.cancelNotificationTimer(); + + let template = types[notification.type]; + Templates.render(template, notification) + .then((html, js) => { + $('#qtracker-notifications').html(html); + Templates.runTemplateJS(js); + + notificationTimeoutHandle = setTimeout(() => { + $('#qtracker-notifications').find('.alert').alert('close'); + }, NOTIFICATION_DURATION); + }) + .catch((error) => { + console.error(error); + throw error; + }); + }; + /** + * @method handleFormSubmissionFailure + * @param {boolean} show + * @private + */ + BlockFormManager.prototype.toggleUpdateButton = function (show) { + if (show) { + Str.get_string('update', 'core').then(function (updateStr) { + this.form.find(SELECTORS.SUBMIT_BUTTON).html(updateStr); + }.bind(this)); + } else { + Str.get_string('submitnewissue', 'local_qtracker').then(function (updateStr) { + this.form.find(SELECTORS.SUBMIT_BUTTON).html(updateStr); + }.bind(this)); + } + }; + /** + * @method handleFormSubmissionFailure + * @param {boolean} show + * @private + */ + BlockFormManager.prototype.toggleDeleteButton = function (show) { + const context = { + type: "button", + classes: "col-auto", + label: "Delete", + id: "qtracker-delete", + }; + + let deleteButton = this.form.find(SELECTORS.DELETE_BUTTON); + if (deleteButton.length == 0 && show) { + Templates.render('local_qtracker/button', context) + .then(function (html, js) { + var container = this.form.find('button').closest(".form-row"); + Templates.appendNodeContents(container, html, js); + this.form.find('#qtracker-delete').on('click', function () { + this.deleteIssue(); + }.bind(this)); + }.bind(this)); + } else { + if (show) { + deleteButton.show(); + } else { + deleteButton.hide(); + } + } + }; + + /** + * @method handleFormSubmissionFailure + * @param {string} newaction + * @private + */ + BlockFormManager.prototype.setAction = function (newaction) { + + this.form.data('action', newaction); + }; + + /** + * Private method + * + * @method submitFormAjax + * @private + * @param {Event} e Form submission event. + */ + BlockFormManager.prototype.submitFormAjax = function (e) { + // We don't want to do a real form submission. + e.preventDefault(); + e.stopPropagation(); + + if (!this.validateForm()) { + return; + } + + + if (this.issueManager.getActiveIssue().isSaved === true) { + this.editIssue(); + } else { + this.createIssue(); + } + /* + Var state = this.issueManager.getActiveIssue().getState(); + switch (state) { + case Issue.STATES.NEW: + this.createIssue(); + break; + case Issue.STATES.EXISTING: + this.editIssue(); + break; + case Issue.STATES.DELETED: + this.issueManager.getActiveIssue().changeState(Issue.STATES.NEW) + this.createIssue(); + break; + default: + break; + }*/ + }; + + BlockFormManager.prototype.validateForm = function () { + let valid = true; + VALIDATION_ELEMENTS.forEach(selector => { + let element = this.form.find(selector); + if (element.val() != "" && element.prop("validity").valid) { + element.removeClass("is-invalid").addClass("is-valid"); + } else { + element.removeClass("is-valid").addClass("is-invalid"); + valid = false; + } + }); + return valid; + }; + + BlockFormManager.prototype.resetValidation = function () { + VALIDATION_ELEMENTS.forEach(selector => { + let element = this.form.find(selector); + element.removeClass("is-invalid").removeClass("is-valid"); + }); + }; + + /** + * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed. + * + * @method submitForm + * @param {Event} e Form submission event. + * @private + */ + BlockFormManager.prototype.submitForm = function (e) { + e.preventDefault(); + this.form.submit(); + }; + + return /** @alias module:local_qtracker/BlockFormManager */ { + + /** + * Initialise the module. + * + * @method init + * @param {string} selector The selector used to find the form for to use for this module. + * @param {string} issueids The ids of existing issues to load. + * @param {int} contextid + * @return {BlockFormManager} + */ + init: function (selector, issueids, contextid) { + return new BlockFormManager(selector, issueids, contextid); + } + }; + }); diff --git a/amd/src/dropdown.js b/amd/src/dropdown.js new file mode 100644 index 0000000..0d7ccb1 --- /dev/null +++ b/amd/src/dropdown.js @@ -0,0 +1,287 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Manager for managing table of questions with issues. + * + * @module local_qtracker/Dropdown + * @class Dropdown + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import $ from 'jquery'; +import Templates from 'core/templates'; +import { get_string as getString } from 'core/str'; +import DropdownEvents from 'local_qtracker/dropdown_events'; +import { loadIssuesData } from 'local_qtracker/api_helpers'; + +var SELECTORS = { + DROPDOWN: '[data-region="dropdown"]', + SEARCH: 'input[type="search"]', + ITEM: '[data-region="item"]', +}; + +/** + * Constructor + * @constructor + * @param {String} selector used to find triggers for the new group modal. + * @param {int} contextid + * + * Each call to init gets it's own instance of this class. + */ +class Dropdown { + loading = null; + root = null; + items = new Map(); + activeItems = new Map(); + isSearching = false; + + constructor(root, search = true) { + this.root = $(root); // Root element dropdown-menu + this.dropdown = this.root.find(SELECTORS.DROPDOWN); + this.search = search; + + this.render = this.renderItems.bind(this); + this.registerEventListeners = this.registerEventListeners.bind(this); + + if (this.search) { + this.registerEventListeners(); + } + } + + + + /** + * Get the dropdown element of this dropdown. + * + * @method getDropdown + * @return {object} jQuery object + */ + getDropdown() { + return this.dropdown; + }; + + /** + * Get the dropdown element of this dropdown. + * + * @method getDropdown + * @return {object} jQuery object + */ + getRoot() { + return this.root; + }; + + /** + * Set up all of the event handling for the modal. + * + * @method registerEventListeners + */ + registerEventListeners() { + // Handle the clicking of an item. + this.getDropdown().on('click', SELECTORS.ITEM, function (e) { + let element = $(e.currentTarget); + if (element.hasClass("disabled")) { + return; + } + var clickEvent = $.Event(DropdownEvents.click); + this.getRoot().trigger(clickEvent, [element]); + + if (!clickEvent.isDefaultPrevented()) { + e.preventDefault(); + } + }.bind(this)); + + this.registerSearchListener(); + } + + + /** + * Register a listener to close the dialogue when the save button is pressed. + * + * @method registerSearch + */ + registerSearchListener() { + + this.getDropdown().find(SELECTORS.SEARCH).on('input', function (e) { + + let str = $(e.target).val(); + var searchEvent = $.Event(DropdownEvents.search, str); + this.getRoot().trigger(searchEvent, [str]); + + if (!searchEvent.isDefaultPrevented()) { + e.preventDefault(); + if (str.length > 0) { + this.isSearching = true; + } else { + this.isSearching = false; + } + this.renderItems(); + } + }.bind(this)); + }; + + + getActiveItems() { + + return this.activeItems; + } + + setItemStatus(key, active = true) { + if (active) { + let item = this.getItems().get(key); + this.activeItems.set(key, item); + } else { + this.activeItems.delete(key); + } + } + + reset() { + this.isSearching = false; + this.getDropdown().find(SELECTORS.SEARCH).val(""); + this.renderItems() + } + + async render() { + let context = { + trigger: { + "key": "fa-times", + "title": "Close", + "alt": "Close pane", + "extraclasses": "", + "unmappedIcon": false + } + }; + //let self = this; + await Templates.render('local_qtracker/dropdown', context).then((html, js) => { + Templates.replaceNodeContents(this.getDropdown(), html, js); + }); + } + + async generateItems() { + let elements = []; + let items = this.isSearching ? this.getItems() : this.getActiveItems(); + if (items.size === 0) { + let element = $('
') + .addClass("dropdown-item disabled") + .attr("data-region", 'item') + .html(await getString('noitems', 'local_qtracker')) + .prop('outerHTML'); + elements.push(element) + } else { + // map ;; index(id), html + items.forEach((html, key) => { + let element = $('
') + .addClass("dropdown-item") + .addClass(() => { + if (this.isActiveItem(key)) { + return "active"; + } + }) + .attr("data-value", key) + .attr("data-region", 'item') + .html(html).prop('outerHTML'); + elements.push(element) + }); + } + return elements; + } + + async renderItems() { + this.empty(); + let elements = await this.generateItems(); + this.getDropdown().append(elements); + //{{{text}}} + } + + /** + * + * @param {*} items tuples [id, html] + * @param {*} active + */ + setItems(items, active = false) { + if (active) { + this.activeItems = new Map(); + items.forEach(item => this.activeItems.set(item[0], item[1])); + } else { + this.items = new Map(); + items.forEach(item => this.items.set(item[0], item[1])); + } + } + + getItems() { + return this.items; + } + + getAllItems() { + return Array.prototype.concat(this.items, this.getActiveItems()); + } + + isActiveItem(key) { + return this.getActiveItems().has(key); + } + + async search(str) { + if (str.length > 0) { + this.isSearching = true; + } else { + this.isSearching = false; + } + + let criteria = []; + if (str.startsWith('#')) { + let id = parseInt(str.substr(1)); + criteria.push({ key: 'id', value: id }); + } else { + if (str.length > 2) str += "%" + criteria.push({ key: 'title', value: str }); + } + + let issuesResponse = await loadIssuesData(criteria); + let issues = issuesResponse.issues; + this.setItems(issues); + return issues; + } + + setTitle(html) { + $('.qtracker-sidebar-title').html(html); + } + + setLoading(show = true) { + if (show) { + $('.qtracker-sidebar-content .loading').addClass("show"); + this.loading = true; + } else { + $('.qtracker-sidebar-content .loading').removeClass("show"); + this.loading = false; + } + } + + empty() { + this.getDropdown().find(SELECTORS.ITEM).remove(); + } + + addTemplateItem(html, js) { + Templates.appendNodeContents('.qtracker-sidebar-content .qtracker-items', html, js); + } + + getElements() { + return $('.qtracker-sidebar-content .qtracker-items').children(); + } + +} + +export default Dropdown; diff --git a/amd/src/dropdown_events.js b/amd/src/dropdown_events.js new file mode 100644 index 0000000..2d96657 --- /dev/null +++ b/amd/src/dropdown_events.js @@ -0,0 +1,15 @@ +/** + * Events for the drawer. + * + * @module local_qtracker/DropdownEvents + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +export default { + search: 'dropdown:search', + click: 'dropdown:click', + change: 'dropdown:change', + +}; diff --git a/amd/src/issue.js b/amd/src/issue.js new file mode 100644 index 0000000..05a0cc0 --- /dev/null +++ b/amd/src/issue.js @@ -0,0 +1,165 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Module for representing a question issue. + * + * @module local_qtracker/Issue + * @class Issue + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/str', 'core/ajax'], function ($, Str, Ajax) { + + /** + * Constructor + * + * @param {int} id + * @param {int} slot + * @param {int} contextid + * + * Each call gets it's own instance of this class. + */ + var Issue = function (id = null, slot = null, contextid) { + this.id = id; + this.slot = slot; + this.contextid = contextid; + }; + + Issue.STATES = { + NEW: "new", + OPEN: "open", + CLOSED: "closed", + }; + + /** + * @var {int} id The id of this issue + * @private + */ + Issue.prototype.id = null; + + /** + * @var {int} id The slot for this issue + * @private + */ + Issue.prototype.slot = null; + + /** + * @var {string} title The title for this issue + * @private + */ + Issue.prototype.title = ""; + + /** + * @var {string} title The description for this issue + * @private + */ + Issue.prototype.description = ""; + + Issue.prototype.contextid = null; + + Issue.prototype.isSaved = false; + + Issue.prototype.state = Issue.STATES.NEW; + + /** + * Initialise the class. + * + * @private + * @param {int} id + */ + Issue.prototype.setId = function (id) { + this.id = id; + }; + + /** + * Initialise the class. + * + * @param {String} selector used to find triggers for the new group modal. + * @private + * @return {Promise} + */ + Issue.prototype.getId = function () { + return this.id; + }; + + Issue.prototype.getSlot = function () { + return this.slot; + }; + + Issue.prototype.getTitle = function () { + return this.title; + }; + + Issue.prototype.setTitle = function (title) { + this.title = title; + }; + + Issue.prototype.getDescription = function () { + return this.description; + }; + + + Issue.prototype.setDescription = function (description) { + this.description = description; + }; + + Issue.prototype.changeState = function (state) { + this.state = state; + }; + + Issue.prototype.getState = function () { + return this.state; + }; + + Issue.prototype.getContextid = function () { + return this.contextid; + }; + + /** + * @return {Promise} + * @param {int} id + */ + Issue.loadData = function (id) { + return Ajax.call([ + { methodname: 'local_qtracker_get_issue', args: { issueid: id } } + ])[0]; + }; + + Issue.load = async function (id) { + let data = await Issue.loadData(id); + let issueData = data.issue; + let issue = new Issue(issueData.id, issueData.slot, issueData.contextid); + issue.setTitle(issueData.title); + issue.setDescription(issueData.description); + return issue; + } + + Issue.prototype.save = async function () { + let result = await Ajax.call([{ + methodname: 'local_qtracker_edit_issue', + args: { + issueid: this.getId(), + issuetitle: this.getTitle(), + issuedescription: this.getDescription(), + }, + }])[0]; + return result + } + + return Issue; +}); diff --git a/amd/src/issue_comment_controls.js b/amd/src/issue_comment_controls.js new file mode 100644 index 0000000..660fad3 --- /dev/null +++ b/amd/src/issue_comment_controls.js @@ -0,0 +1,131 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Class for handling question issue page. + * + * @module local_qtracker/QuestionIssuePage + * @class QuestionIssuePage + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import $ from 'jquery'; +import * as Str from 'core/str'; +import ModalFactory from 'core/modal_factory'; +import ModalEvents from 'core/modal_events'; + +/** + * Constructor + * @constructor + * @param {int} commentid + * + * Each call gets it's own instance of this class. + */ +class IssueCommentControls { + constructor(commentid) { + this.commentid = commentid; + this.init(); + } + + async init() { + this.registerDeleteButtonListener() + this.registerNotifyButtonListener() + } + + async registerDeleteButtonListener() { + + let trigger = $('#comment_delete_' + this.commentid); + let strObj = [ + { + key: 'confirm', + component: 'local_qtracker' + }, + { + key: 'confirmdeletecomment', + component: 'local_qtracker' + }, + { + key: 'deletecomment', + component: 'local_qtracker' + } + ]; + + let strings = await Str.get_strings(strObj); + + let modal = await ModalFactory.create({ + type: ModalFactory.types.SAVE_CANCEL, + title: strings[2], + body: strings[1], + }, trigger) + + modal.setSaveButtonText(strings[0]) + modal.getRoot().on(ModalEvents.save, function (e) { + // Stop the default save button behaviour which is to close the modal. + e.preventDefault(); + let form = $('#comment_form_' + this.commentid); + $('').attr({ + type: "hidden", + name: "deletecommentid", + value: this.commentid, + }).appendTo(form); + form.submit(); + }.bind(this)); + } + + async registerNotifyButtonListener() { + + let trigger = $('#comment_message_' + this.commentid); + let strObj = [ + { + key: 'confirm', + component: 'local_qtracker' + }, + { + key: 'confirmsendcomment', + component: 'local_qtracker' + }, + { + key: 'sendcomment', + component: 'local_qtracker' + } + ]; + + let strings = await Str.get_strings(strObj); + + let modal = await ModalFactory.create({ + type: ModalFactory.types.SAVE_CANCEL, + title: strings[2], + body: strings[1], + }, trigger) + + modal.setSaveButtonText(strings[0]) + modal.getRoot().on(ModalEvents.save, function (e) { + // Stop the default save button behaviour which is to close the modal. + e.preventDefault(); + let form = $('#comment_form_' + this.commentid); + $('').attr({ + type: "hidden", + name: "notifycommentid", + value: this.commentid, + }).appendTo(form); + form.submit(); + }.bind(this)); + } +} + + +export default IssueCommentControls; diff --git a/amd/src/issue_manager.js b/amd/src/issue_manager.js new file mode 100644 index 0000000..05ae1c4 --- /dev/null +++ b/amd/src/issue_manager.js @@ -0,0 +1,102 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Manager for managing question issues. + * + * @module local_qtracker/IssueManager + * @class IssueManager + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'local_qtracker/issue'], function ($, Issue) { + + /** + * Constructor + * @constructor + * @param {String} selector used to find triggers for the new group modal. + * @param {int} contextid + * + * Each call to init gets it's own instance of this class. + */ + var IssueManager = function () { }; + + /** + * @var {Form} form + * @private + */ + IssueManager.prototype.issues = new Map(); + + IssueManager.prototype.activeIssue = null; + + IssueManager.prototype.getActiveIssue = function () { + return this.activeIssue; + }; + + IssueManager.prototype.setActiveIssue = function (slot) { + let newIssue = this.getIssueBySlot(slot); + this.activeIssue = newIssue; + return newIssue; + }; + + /** + * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed. + * + * @method submitForm + * @param slot + * @private + * @return + */ + IssueManager.prototype.getIssueBySlot = function (slot) { + return this.issues.get(slot); + }; + + IssueManager.prototype.getIssueById = function (id) { + for (const [slot, issue] of this.issues) { + if (issue.getId() !== null && issue.getId() === id) { + return issue; + } + } + return false; + }; + + IssueManager.prototype.addIssue = function (issue) { + this.issues.set(issue.getSlot(), issue); + }; + + IssueManager.prototype.loadIssues = function (issueids = []) { + let promises = []; + for (let i = 0; i < issueids.length; i++) { + const id = issueids[i]; + let promise = Issue.loadData(id).then((response) => { + let issue = this.getIssueBySlot(response.issue.slot); + if (!issue) { + issue = new Issue(response.issue.id, response.issue.slot); + } + issue.setId(response.issue.id); + issue.setTitle(response.issue.title); + issue.setDescription(response.issue.description); + issue.isSaved = true;// ChangeState(Issue.STATES.EXISTING); + this.addIssue(issue); + }); + promises.push(promise); + } + return Promise.all(promises); + }; + + return IssueManager; +}); diff --git a/amd/src/question_issue_page.js b/amd/src/question_issue_page.js new file mode 100644 index 0000000..ef9fba7 --- /dev/null +++ b/amd/src/question_issue_page.js @@ -0,0 +1,555 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Class for handling question issue page. + * + * @module local_qtracker/QuestionIssuePage + * @class QuestionIssuePage + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import $ from 'jquery'; +import Templates from 'core/templates'; +import Ajax from 'core/ajax'; +import url from 'core/url'; +import * as Str from 'core/str'; +import Sidebar from 'local_qtracker/sidebar'; +import Dropdown from 'local_qtracker/dropdown'; +import Issue from 'local_qtracker/issue'; +import ModalFactory from 'core/modal_factory'; +import ModalEvents from 'core/modal_events'; +import DropdownEvents from 'local_qtracker/dropdown_events'; +import { loadIssuesData } from 'local_qtracker/api_helpers'; + +var SELECTORS = { + TITLE: '[data-region="issuetitle"]', + TITLE_TEXT: '[data-region="issuetitle-text"]', + TITLE_INPUT: '[data-region="issuetitle-input"]', +}; + +/** + * Constructor + * @constructor + * @param {String} courseid + * @param {int} questionid + * @param {int} issueid + * + * Each call gets it's own instance of this class. + */ +class QuestionIssuePage { + courseid = null; + questionid = null; + issueid = null; + issue = null; + parents = []; + filter = new Set(['Open', 'New']) + + constructor(courseid, questionid, issueid) { + this.courseid = courseid; + this.questionid = questionid; + this.issueid = parseInt(issueid); + this.issue = null; + this.loadSettings() + + this.editingTitle = false; + this.init(); + } + + async init() { + this.checkForParents() + this.initSidebar() + this.initDropdowns(); + this.registerEditTitleButtonListener() + } + + async checkForParents() { + + let parentsData = await this.loadIssueParents(this.issueid); + if (parentsData.parents.length > 0) { + this.parents = parentsData.parents; + let supersededids = this.parents.map((parent) => { + return $('') + .attr("href", this._getIssueUrl(parent.id)) + .html("#" + parent.id).prop('outerHTML'); + }).join(", "); + this.notify({ + message: await Str.get_string('issuesuperseded', 'local_qtracker', supersededids), + announce: false, + type: "warning", + }, '#qtracker-superseded'); + } + } + + loadSettings() { + let filterData = JSON.parse(sessionStorage.getItem('local_qtracker_issue_page_filter')) + if (filterData !== null) { + filterData.forEach(this.filter.add, this.filter) + } + } + + saveSettings() { + let filterData = Array.from(this.filter); + sessionStorage.setItem('local_qtracker_issue_page_filter', JSON.stringify(filterData)) + } + + + async loadChildren() { + let childrenData = await this.loadIssueChildren(this.issueid); + let children = childrenData.children; + let items = children.map((item) => [item.id, item.title]); + return items; + } + + async getIssue() { + if (this.issue === null) { + this.issue = await Issue.load(this.issueid); + } + return this.issue; + } + + registerEditTitleButtonListener() { + $(".edittitle").children("button").on('click', async function (e) { + let button = $(e.target); + if (!this.editingTitle) { + button.html(await Str.get_string('save', 'core')); + $(SELECTORS.TITLE_INPUT).show(); + $(SELECTORS.TITLE_TEXT).parent("div").hide(); + this.editingTitle = true; + } else { + let title = $(SELECTORS.TITLE_INPUT).val(); + let issue = await this.getIssue(); + issue.setTitle(title); + let response = await issue.save(); + if (response.status) { + button.html(await Str.get_string('edit', 'core')); + $(SELECTORS.TITLE_INPUT).hide(); + $(SELECTORS.TITLE_TEXT).parent("div").show(); + $(SELECTORS.TITLE_TEXT).text(title); + this.editingTitle = false; + } + } + }.bind(this)); + } + + // aside blocks + async initDropdowns() { + let issuesDropdown = new Dropdown('#linkedissues-dropdown'); + issuesDropdown.setItems(await this.loadChildren(), true); + this.updateIssueAsideBlock(issuesDropdown.getActiveItems()); + + // TODO: Remove the right margin of the content container when the grading panel is hidden so that it expands to full-width. + let dropdown = issuesDropdown; + dropdown.getRoot().on(DropdownEvents.search, async function (e, str) { + let criteria = []; + if (str.startsWith('#')) { + let id = parseInt(str.substr(1)); + criteria.push({ key: 'id', value: id }); + } else { + if (str.length > 2) str += "%" + criteria.push({ key: 'title', value: str }); + } + let issuesResponse = await loadIssuesData(criteria); + let issues = issuesResponse.issues; + let items = issues.map((item) => { + let html = item.title + ' #' + item.id + ''; + return [item.id, html]; + }); + issuesDropdown.setItems(items); + issuesDropdown.renderItems(); + }.bind(this)); + + + dropdown.getRoot().on(DropdownEvents.click, async function (e, element) { + let parentid = parseInt(this.issueid); + let childid = parseInt(element.attr("data-value")); + let active = dropdown.isActiveItem(childid); + if (active) { + let response = await this.deleteIssueRelation(parentid, childid); + if (response.status) { + this.renderSidebarContent();//TODO: update sidebar issues if exists with new status. + issuesDropdown.setItemStatus(childid, !active) + issuesDropdown.reset(); + this.updateIssueAsideBlock(issuesDropdown.getActiveItems()) + } + } else { + let modal = await ModalFactory.create({ + title: await Str.get_string('subsumeissue', 'local_qtracker'), + body: await Str.get_string('subsumeissueconfirm', 'local_qtracker', { child: childid, parent: parentid }), + type: ModalFactory.types.SAVE_CANCEL, + large: false, + }) + modal.setSaveButtonText(await Str.get_string('confirm', 'core')); + modal.getRoot().on(ModalEvents.save, async e => { + // Don't close the modal yet. + e.preventDefault(); + // Submit form data. + let response = await this.setIssueRelation(parentid, childid); + if (response.status) { + this.renderSidebarContent();//TODO: update sidebar issues if exists with new status. + issuesDropdown.setItemStatus(childid, !active) + issuesDropdown.reset(); + this.updateIssueAsideBlock(issuesDropdown.getActiveItems()) + } else { + let issueurl = $('') + .attr("href", this._getIssueUrl(childid)) + .html("#" + childid).prop('outerHTML'); + + this.notify({ + message: await Str.get_string('errorsubsumingissue', 'local_qtracker', issueurl), + announce: true, + closebutton: true, + type: "error", + }, '#qtracker-notifications'); + } + modal.destroy(); + + //submitEditFormAjax(link, getBody, modal, userEnrolmentId, container.dataset); + }); + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, () => { + // Destroy when hidden. + modal.destroy(); + }); + // Show the modal. + modal.show(); + } + }.bind(this)) + + issuesDropdown.renderItems(); + } + + async updateIssueAsideBlock(items) { + let elements = [] + + if (items.size === 0) { + let element = $('
') + .addClass("dropdown-item disabled") + .html(await Str.get_string('noitems', 'local_qtracker')) + .prop('outerHTML'); + elements.push(element) + } + + items.forEach((html, key) => { + let element = $('') + .addClass("list-item border-0 p-1") + .attr("href", this._getIssueUrl(key)) + .html(html).prop('outerHTML'); + //
{{{text}}}
+ elements.push(element); + }); + $(".linkedissues-list").html(elements); + } + + _getIssueUrl(issueid) { + let issueurl = url.relativeUrl('/local/qtracker/issue.php', { + courseid: this.courseid, + issueid: issueid, + }); + return issueurl; + } + + async renderSidebarContent() { + let state = null; + this.sidebar.setLoading(true); + this.sidebar.empty(); + + // Get issues data. + let issuesResponse = await this.loadIssues(this.questionid, state); + let issues = issuesResponse.issues; + + // Get users data. + let userids = [...new Set(issues.map(issue => issue.userid))]; + let usersData = await this.loadUsersData(userids); + + // Render issue items. + let promises = []; + issues.forEach(async issueData => { + let userData = usersData.find(({ id }) => id === issueData.userid); + if (issueData.id == this.issueid) { + return; + } + promises.push(this.addIssueItem(issueData, userData)); + }); + + self = this; + // When all issue item promises are resolved. + $.when.apply($, promises).done(function () { + self.sidebar.setLoading(false); + $.each(arguments, (index, argument) => { + self.sidebar.addTemplateItem(argument.html, argument.js); + }); + self.applyFilter(); + }).catch(e => { + console.error(e); + }); + } + + async initSidebar() { + let active = this.filter.has("Closed"); + let sidebarOptions = [{ "name": "toggleclosed", "text": "Show closed issues", "value": 0, "checkbox": true, "active": active }]; + this.sidebar = new Sidebar('#question-issues-sidebar', true, "left", false, '30%', '1.25rem', false, sidebarOptions); + + await this.sidebar.render(); + + this.sidebar.empty(); + this.sidebar.setLoading(true); + + // Get question title. + let questionData = await this.loadQuestionData(this.questionid); + let question = questionData.question; + let questionEditUrl = this.getQuestionEditUrl(this.courseid, this.questionid); + let link = $('').attr("href", questionEditUrl).html(question.name + " #" + question.id); + this.sidebar.setTitle(link); + this.sidebar.show(); + + await this.renderSidebarContent(); + + // Add logic to sidebar actions (dropdowns) + this.sidebar.getContainer().on('click', async function (e) { + let element = $(e.target); + if (element.hasClass("dropdown-item")) { + let dropdownItem = element.attr("data-name"); + let itemValue = parseInt(element.attr("data-value")); + switch (dropdownItem) { + case "toggleclosed": // Sidebar toolbar filter + if (element.is(':checked')) { + this.filter.add('Closed'); + element.prop('checked', true); + } else { + this.filter.delete('Closed'); + element.prop('checked', false); + } + this.applyFilter(); + this.saveSettings(); + break; + case "subsume": // Sidebar item action + //TODO: add issue menu to the sidebar items + let parentid = this.issueid; + let childid = itemValue; + let response = await this.setIssueRelation(parentid, childid); + if (response.status) { + this.renderSidebarContent() + } + break; + default: + break; + } + } + }.bind(this)) + + window.closeIssuesPane = function () { this.sidebar.hide() }.bind(this); + window.toggleIssuesPane = function () { this.sidebar.togglePane() }.bind(this); + } + + applyFilter() { + this.resetFilter(); + let self = this; + this.sidebar.getItems().each(function () { + if (!self.filter.has($(this).find(".badge").text())) { + $(this).hide(); + } + }); + } + + resetFilter() { + this.sidebar.getItems().each(function () { + $(this).show(); + }); + } + + /** + * + * @param {object} issueData + * @param {object} userData + * @return {Promise} + */ + async addIssueItem(issueData, userData, extraClasses = "") { + // Fetch user data. + let issueurl = url.relativeUrl('/local/qtracker/issue.php', { + courseid: this.courseid, + issueid: issueData.id, + }); + let userurl = url.relativeUrl('/user/view.php', { + course: this.courseid, + id: userData.id, + }); + + let actions = { + "trigger": { + "key": "fa-ellipsis-h", + "title": "Options", + "alt": "Show options", + "extraclasses": "", + "unmappedIcon": false + }, + "header": false, + "items": [ + { "name": "subsume", "text": "Subsume", "value": issueData.id }, + ] + } + // Render issues pane + let paneContext = { + issueurl: issueurl, + userurl: userurl, + profileimageurl: userData.profileimageurlsmall, + fullname: userData.fullname, + timecreated: issueData.timecreated, + id: issueData.id, + title: issueData.title, + description: issueData.description, + extraclasses: extraClasses + //actions: actions // TODO: finish this + }; + let state = issueData.state; + paneContext[state] = true; + + return Templates.render('local_qtracker/sidebar_item_issue', paneContext) + .then(function (html, js) { + return { html: html, js: js }; + }); + } + + async loadIssues(id, state = null) { + let criteria = [ + { key: 'questionid', value: id }, + ]; + if (state) { + criteria.push({ key: 'state', value: state }); + } + let issuesData = await Ajax.call([{ + methodname: 'local_qtracker_get_issues', + args: { criteria: criteria } + }])[0]; + + return issuesData; + } + + async loadUsersData(ids) { + let usersData = await Ajax.call([{ + methodname: 'core_user_get_users_by_field', + args: { + field: 'id', + values: ids + } + }])[0]; + return usersData; + } + + async setIssueRelation(parentid, childid) { + let result = await Ajax.call([{ + methodname: 'local_qtracker_set_issue_relation', + args: { + parentid: parentid, + childid: childid, + } + }])[0]; + return result + } + + + async deleteIssueRelation(parentid, childid) { + let result = await Ajax.call([{ + methodname: 'local_qtracker_delete_issue_relation', + args: { + parentid: parentid, + childid: childid, + } + }])[0]; + return result + } + + getQuestionEditUrl(courseid, questionid) { + let returnurl = encodeURIComponent(location.pathname + location.search); + let editurl = url.relativeUrl('/question/question.php', { + courseid: courseid, + id: questionid, + returnurl: returnurl, + }); + return editurl; + } + + decodeHTML(html) { + var doc = new DOMParser().parseFromString(html, "text/html"); + return doc.documentElement.textContent; + } + + async loadQuestionData(id) { + let userData = await Ajax.call([{ + methodname: 'local_qtracker_get_question', + args: { + id: id + } + }])[0]; + return userData; + } + + async loadIssueParents(id) { + let userData; + userData = await Ajax.call([{ + methodname: 'local_qtracker_get_issue_parents', + args: { + issueid: id + } + }])[0]; + return userData; + } + + async loadIssueChildren(id) { + let userData; + userData = await Ajax.call([{ + methodname: 'local_qtracker_get_issue_children', + args: { + issueid: id + } + }])[0]; + return userData; + } + + notify(notification, selector = null) { + notification = $.extend({ + closebutton: false, + announce: false, + type: 'error', + extraclasses: "show", + }, notification); + + let types = { + 'success': 'core/notification_success', + 'info': 'core/notification_info', + 'warning': 'core/notification_warning', + 'error': 'core/notification_error', + }; + + let template = types[notification.type]; + Templates.render(template, notification) + .then((html, js) => { + if (selector === null) { + $('#qtracker-notifications').append(html); + } else { + $(selector).append(html); + } + Templates.runTemplateJS(js); + }) + .catch((error) => { + console.error(error); + throw error; + }); + }; +} + +export default QuestionIssuePage; diff --git a/amd/src/questions_table_page.js b/amd/src/questions_table_page.js new file mode 100644 index 0000000..4867840 --- /dev/null +++ b/amd/src/questions_table_page.js @@ -0,0 +1,186 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Class for handling page with table of questions with issues. + * + * @module local_qtracker/QuestionsTablePage + * @class QuestionsTablePage + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import $ from 'jquery'; +import Templates from 'core/templates'; +import Ajax from 'core/ajax'; +import url from 'core/url'; +import Sidebar from 'local_qtracker/sidebar'; + +/** + * Constructor + * @constructor + * @param {String} selector used to find triggers for the new group modal. + * @param {int} contextid + * + * Each call to init gets it's own instance of this class. + */ +class QuestionsTablePage { + courseid = null; + + constructor(courseid) { + this.courseid = courseid; + this.sidebar = new Sidebar('#questions-table-sidebar', false, "right", false, '40%'); + this.init(); + } + + async init() { + + await this.sidebar.render(); + window.showIssuesInPane = async function (id, state = null) { + this.sidebar.empty(); + this.sidebar.setLoading(true); + + // Get question title. + let questionData = await this.loadQuestionData(id); + let question = questionData.question; + let questionEditUrl = this.getQuestionEditUrl(this.courseid, id); + let link = $('').attr("href", questionEditUrl).html(question.name + " #" + question.id); + this.sidebar.setTitle(link); + this.sidebar.show(); + + // Get issues data. + let issuesResponse = await this.loadIssues(id, state); + let issues = issuesResponse.issues; + + // Get users data. + let userids = [...new Set(issues.map(issue => issue.userid))]; + let usersData = await this.loadUsersData(userids); + + // Render issue items. + let promises = []; + issues.forEach(async issueData => { + let userData = usersData.find(({ id }) => id === issueData.userid); + promises.push(this.addIssueItem(issueData, userData)); + }); + + self = this; + // When all issue item promises are resolved. + $.when.apply($, promises).done(function () { + self.sidebar.setLoading(false); + $.each(arguments, (index, argument) => { + self.sidebar.addTemplateItem(argument.html, argument.js); + }); + }).catch(e => { + console.error(e); + }); + + }.bind(this); + + window.closeIssuesPane = function () { this.sidebar.hide() }.bind(this); + window.toggleIssuesPane = function () { this.sidebar.togglePane() }.bind(this); + + } + + /** + * + * @param {object} issueData + * @param {object} userData + * @return {Promise} + */ + async addIssueItem(issueData, userData) { + // Fetch user data. + let issueurl = url.relativeUrl('/local/qtracker/issue.php', { + courseid: this.courseid, + issueid: issueData.id, + }); + let userurl = url.relativeUrl('/user/view.php', { + course: this.courseid, + id: userData.id, + }); + + // Render issues pane + let paneContext = { + issueurl: issueurl, + userurl: userurl, + profileimageurl: userData.profileimageurlsmall, + fullname: userData.fullname, + timecreated: issueData.timecreated, + id: issueData.id, + title: issueData.title, + description: issueData.description, + }; + let state = issueData.state; + paneContext[state] = true; + + return Templates.render('local_qtracker/sidebar_item_issue', paneContext) + .then(function (html, js) { + return { html: html, js: js }; + }); + } + + async loadIssues(id, state = null) { + let criteria = [ + { key: 'questionid', value: id }, + ]; + if (state) { + criteria.push({ key: 'state', value: state }); + } + let issuesData = await Ajax.call([{ + methodname: 'local_qtracker_get_issues', + args: { criteria: criteria } + }])[0]; + + return issuesData; + } + + async loadUsersData(ids) { + let usersData = await Ajax.call([{ + methodname: 'core_user_get_users_by_field', + args: { + field: 'id', + values: ids + } + }])[0]; + return usersData; + } + + getQuestionEditUrl(courseid, questionid) { + let returnurl = encodeURIComponent(location.pathname + location.search); + let editurl = url.relativeUrl('/question/question.php', { + courseid: courseid, + id: questionid, + returnurl: returnurl, + }); + return editurl; + } + + decodeHTML(html) { + var doc = new DOMParser().parseFromString(html, "text/html"); + return doc.documentElement.textContent; + } + + async loadQuestionData(id) { + let userData = await Ajax.call([{ + methodname: 'local_qtracker_get_question', + args: { + id: id + } + }])[0]; + return userData; + } +} + +export default QuestionsTablePage; diff --git a/amd/src/sidebar.js b/amd/src/sidebar.js new file mode 100644 index 0000000..a12660a --- /dev/null +++ b/amd/src/sidebar.js @@ -0,0 +1,216 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Manager for managing a sidebar. + * + * @module local_qtracker/Sidebar + * @class Sidebar + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import $ from 'jquery'; +import Templates from 'core/templates'; + +/** + * Constructor + * @constructor + * @param {String} selector used to find triggers for the new group modal. + * @param {int} contextid + * + * Each call to init gets it's own instance of this class. + */ +class Sidebar { + hidden = null; + loading = null; + width = null; + margin = null; + container = null; + closable = null; + options = null; + + constructor(container, show = false, side = 'right', loading = false, width = '40%', margin = '0px', closable = true, options = []) { + this.container = $(container); // Container element + this.hidden = !show; + this.visible = show; + this.side = side; + this.loading = loading; + this.width = width; + this.margin = margin; + + this.closable = closable; + this.options = options; + + this.mql = window.matchMedia('(min-width: 768px)'); + this.mql.addEventListener('change', this.screenTest.bind(this)); + + this.render = this.render.bind(this); + } + + screenTest(e) { + if (!this.visible) { + return; + } + if (e.matches) { + /* the viewport is 768px pixels wide or more */ + $('#qtracker-sidebar').css('width', 'calc(' + this.width + ' - -1.25rem)'); + $('.qtracker-push-pane-over').css('padding-' + this.getSide(), 'calc(' + this.width + ' - -' + this.margin + ')'); + } else { + /* the viewport is more than 768px pixels wide or less */ + $('#qtracker-sidebar').css('width', '100%'); + $('.qtracker-push-pane-over').css('padding-' + this.getSide(), '0'); + } + } + + async render() { + let self = this; + + let context = {}; + if (this.closable) { + context.close = { + "key": "fa-times", + "title": "Close", + "alt": "Close pane", + "extraclasses": "", + "unmappedIcon": false + }; + } + if (this.options.length > 0) { + context.options = { + "trigger": { + "key": "fa-filter", + "title": "Options", + "alt": "Show options", + "extraclasses": "", + "unmappedIcon": false + }, + "header": false, + "items": self.options, + }; + } + + //let self = this; + await Templates.render('local_qtracker/sidebar', context).then((html, js) => { + Templates.replaceNodeContents(this.container, html, js); + this.setVisibility(!this.hidden); + this.setLoading(this.loading); + this.setSide(this.side); + this.screenTest(this.mql) + }); + } + + getSide() { + return this.side; + } + + getOppositeSide() { + return this.side == 'left' ? 'right' : 'left'; + } + + /** + * + * @param {*} width The sidebar width + * @param {*} width2 The existing content width + */ + setWidth(width, width2) { + this.width = width; + this.width2 = width2; + + this.screenTest(this.mql); + } + + isMobileWidth() { + return !this.mql.matches; + } + setSide(side) { + if (side == 'right') { + $('#qtracker-sidebar').addClass('qtracker-sidebar-right'); + $('#qtracker-sidebar').removeClass('qtracker-sidebar-left'); + } else if (side == 'left') { + $('#qtracker-sidebar').addClass('qtracker-sidebar-left'); + $('#qtracker-sidebar').removeClass('qtracker-sidebar-right'); + } + } + + setTitle(html) { + $('.qtracker-sidebar-title').html(html); + } + + setLoading(show = true) { + if (show) { + $('.qtracker-sidebar-content .loading').addClass("show"); + this.loading = true; + } else { + $('.qtracker-sidebar-content .loading').removeClass("show"); + this.loading = false; + } + } + + empty() { + $('.qtracker-sidebar-content .qtracker-items').empty(); + } + + addTemplateItem(html, js) { + Templates.appendNodeContents('.qtracker-sidebar-content .qtracker-items', html, js); + } + + getItems() { + return $('.qtracker-sidebar-content .qtracker-items').children(); + } + + getContainer() { + return this.container; + } + + hide() { + if (!this.hidden) { + this.setVisibility(false); + } + } + + show() { + if (this.hidden) { + this.setVisibility(true); + } + } + + setVisibility(show = true) { + if (show) { + $('.qtracker-push-pane-over').css('padding-' + this.getSide(), 'calc(' + this.width + ' - -' + this.margin + ')'); + $('#qtracker-sidebar').addClass('show'); + this.screenTest(this.mql); + } else { + $('.qtracker-push-pane-over').css('padding-' + this.getSide(), '0'); + $('#qtracker-sidebar').removeClass('show'); + } + this.hidden = !show; + this.visible = show; + } + + togglePane() { + $('.qtracker-container').toggleClass('qtracker-push-pane-over'); + $('#qtracker-sidebar').toggleClass("show"); + this.hidden = !this.hidden; + } + + decodeHTML(html) { + var doc = new DOMParser().parseFromString(html, "text/html"); + return doc.documentElement.textContent; + } +} + +export default Sidebar; diff --git a/backup/moodle2/backup_local_qtracker_plugin.class.php b/backup/moodle2/backup_local_qtracker_plugin.class.php new file mode 100644 index 0000000..36b3f20 --- /dev/null +++ b/backup/moodle2/backup_local_qtracker_plugin.class.php @@ -0,0 +1,130 @@ +. + +/** + * Defines backup_local_qtracker_plugin class + * + * @package local_qtracker + * @author David Rise Knotten + * @author André Storhaug + * @copyright 2021 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Local qtracker backup + * + * @package local_qtracker + * @copyright 2021 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_local_qtracker_plugin extends backup_local_plugin { + + /** + * Define qtracker structure from question entrypoint + * + * @return backup_plugin_element + * @throws base_element_struct_exception + */ + protected function define_course_plugin_structure() { + // Define backup elements + $plugin = $this->get_plugin_element(); + $pluginwrapper = new backup_nested_element($this->get_recommended_name()); + $qtrackerissue = new backup_nested_element('issue', ['id'], [ + 'title', 'description', 'questionid', 'state', 'userid', + 'contextid', 'timecreated', + ]); + + $qtrackercommentwrapper = new backup_nested_element('comments'); + $qtrackercomment = new backup_nested_element('comment', ['id'], [ + 'description', 'userid', 'timecreated', 'mailed' + ]); + + $qtrackerreferencewrapper = new backup_nested_element('references'); + $qtrackerreference = new backup_nested_element('reference', ['id'], [ + 'targetid', 'reftype' + ]); + + // Build the backup tree + $qtrackercommentwrapper->add_child($qtrackercomment); + $qtrackerreferencewrapper->add_child($qtrackerreference); + $qtrackerissue->add_child($qtrackerreferencewrapper); + $qtrackerissue->add_child($qtrackercommentwrapper); + $pluginwrapper->add_child($qtrackerissue); + $plugin->add_child($pluginwrapper); + + // Define sources + $qtrackerissue->set_source_table('local_qtracker_issue', ['contextid' => backup::VAR_CONTEXTID]); + $qtrackercomment->set_source_table('local_qtracker_comment', ['issueid' => backup::VAR_PARENTID]); + $qtrackerreference->set_source_table('local_qtracker_reference', ['sourceid' => backup::VAR_PARENTID]); + + // Define annotations + $qtrackerissue->annotate_ids('user','userid'); + $qtrackerissue->annotate_ids('question','questionid'); + $qtrackercomment->annotate_ids('user','userid'); + + return $plugin; + } + + /** + * Define qtracker structure from question entrypoint + * + * @return backup_plugin_element + * @throws base_element_struct_exception + */ + protected function define_module_plugin_structure() { + // Define backup elements + $plugin = $this->get_plugin_element(); + $pluginwrapper = new backup_nested_element($this->get_recommended_name()); + + $qtrackerissue = new backup_nested_element('issue', ['id'], [ + 'title', 'description', 'questionid', 'questionusageid', 'slot', + 'state', 'userid', 'contextid', 'timecreated', + ]); + + $qtrackercommentwrapper = new backup_nested_element('comments'); + $qtrackercomment = new backup_nested_element('comment', ['id'], [ + 'description', 'userid', 'timecreated', 'mailed' + ]); + + $qtrackerreferencewrapper = new backup_nested_element('references'); + $qtrackerreference = new backup_nested_element('reference', ['id'], [ + 'targetid', 'reftype' + ]); + + // Build the backup tree + $qtrackercommentwrapper->add_child($qtrackercomment); + $qtrackerreferencewrapper->add_child($qtrackerreference); + $qtrackerissue->add_child($qtrackerreferencewrapper); + $qtrackerissue->add_child($qtrackercommentwrapper); + $pluginwrapper->add_child($qtrackerissue); + $plugin->add_child($pluginwrapper); + + // Define sources + $qtrackerissue->set_source_table('local_qtracker_issue', ['contextid' => backup::VAR_CONTEXTID]); + $qtrackercomment->set_source_table('local_qtracker_comment', ['issueid' => backup::VAR_PARENTID]); + $qtrackerreference->set_source_table('local_qtracker_reference', ['sourceid' => backup::VAR_PARENTID]); + + // Define annotations + $qtrackerissue->annotate_ids('user','userid'); + $qtrackerissue->annotate_ids('question','questionid'); + $qtrackercomment->annotate_ids('user','userid'); + + return $plugin; + } +} diff --git a/backup/moodle2/restore_local_qtracker_plugin.class.php b/backup/moodle2/restore_local_qtracker_plugin.class.php new file mode 100644 index 0000000..87f0761 --- /dev/null +++ b/backup/moodle2/restore_local_qtracker_plugin.class.php @@ -0,0 +1,144 @@ +. + +/** + * Defines restore_local_qtracker_plugin class + * + * @package local_qtracker + * @author David Rise Knotten + * @author André Storhaug + * @copyright 2021 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Local qtracker restore + * + * @package local_qtracker + * @copyright 2021 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +class restore_local_qtracker_plugin extends restore_local_plugin { + + /** + * @var array $issues Stores the IDs of the newly created issues. + */ + protected $issues = array(); + + /** + * + */ + protected function define_course_plugin_structure() { + $paths = array(); + + //plugin_local_qtracker_question + $elename = 'issue'; // This defines the postfix of 'process_*' below. + $elepath = $this->get_pathfor('/issue'); + $paths[] = new restore_path_element($elename, $elepath); + + $elename = 'comment'; // This defines the postfix of 'process_*' below. + $elepath = $this->get_pathfor('/comments/comment'); + $paths[] = new restore_path_element($elename, $elepath); + + $elename = 'reference'; // This defines the postfix of 'process_*' below. + $elepath = $this->get_pathfor('/references/reference'); + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; // And we return the interesting paths. + } + + protected function define_module_plugin_structure() { + $paths = array(); + + $elename = 'issue'; + $elepath = $this->get_pathfor('/issue'); + $paths[] = new restore_path_element($elename, $elepath); + + $elename = 'comment'; + $elepath = $this->get_pathfor('/comments/comment'); + $paths[] = new restore_path_element($elename, $elepath); + + $elename = 'reference'; + $elepath = $this->get_pathfor('/references/reference'); + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; + } + + public function process_issue($data) { + global $DB; + $data = (object)$data; + $oldid = $data->id; + $data->questionid = $this->get_mappingid('question', $data->questionid); + + $oldcontextid = $data->contextid; + $newcontextid = $this->get_mappingid('context', $oldcontextid); + $data->userid = $this->get_mappingid('user', $data->userid); + if (isset($data->questionusageid)) { + $data->questionusageid = $this->get_mappingid('question_usage', $data->questionusageid); + } + $data->contextid = $newcontextid; + $newitemid = $DB->insert_record('local_qtracker_issue', $data); + + if (!$newcontextid) { + // Add the array of issues we need to process later. + $data->id = $newitemid; + $data->contextid = $oldcontextid; + $this->issues[$data->id] = $data; + } + + $this->set_mapping('local_qtracker_issue', $oldid, $newitemid); + } + + public function process_comment($data) { + global $DB; + $data = (object)$data; + $oldid = $data->id; + $data->issueid = $this->get_mappingid('local_qtracker_issue', $data->issueid); + $data->userid = $this->get_mappingid('user', $data->userid); + $newitemid = $DB->insert_record('local_qtracker_comment', $data); + $this->set_mapping('local_qtracker_comment', $oldid, $newitemid); + } + + public function process_reference($data) { + global $DB; + $data = (object)$data; + $oldid = $data->id; + $data->sourceid = $this->get_mappingid('local_qtracker_issue', $data->sourceid); + $data->targetid = $this->get_mappingid('local_qtracker_issue', $data->targetid); + $newitemid = $DB->insert_record('local_qtracker_reference', $data); + $this->set_mapping('local_qtracker_reference', $oldid, $newitemid); + } + + /** + * This function is executed after all the tasks in the plan have been finished. + * This must be done here because the activities have not been restored yet. + */ + public function after_restore_module() { + global $DB; + // Need to go through and change the values. + foreach ($this->issues as $issue) { + $updateissue = new stdClass(); + $updateissue->id = $issue->id; + $updateissue->contextid = $this->get_mappingid('context', $issue->contextid); + $DB->update_record('local_qtracker_issue', $updateissue); + } + } +} diff --git a/classes/event/question_deleted_observer.php b/classes/event/question_deleted_observer.php new file mode 100644 index 0000000..f5bd425 --- /dev/null +++ b/classes/event/question_deleted_observer.php @@ -0,0 +1,56 @@ +. + +/** + * Event observers supported by this module. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\event; + +use local_qtracker\issue; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Event observers supported by this module. + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_deleted_observer { + + /** + * Delete related question issues when question is deleted. + * + * @param \core\event\question_deleted $event + */ + public static function question_deleted(\core\event\question_deleted $event) { + global $DB; + // Delete all issues for given question. + $records = $DB->get_records('local_qtracker_issue', ['questionid' => $event->objectid], '', 'id'); + + foreach ($records as $record) { + $issue = issue::load($record->id); + $issue->delete(); + } + } +} diff --git a/classes/external/delete_issue.php b/classes/external/delete_issue.php new file mode 100644 index 0000000..6f47403 --- /dev/null +++ b/classes/external/delete_issue.php @@ -0,0 +1,126 @@ +. + +/** + * External (web service) function calls for deleting a question issue. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/externallib.php"); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use external_value; +use external_function_parameters; +use moodle_exception; +use external_single_structure; +use external_warnings; +use local_qtracker\issue; + +/** + * Class delete_issue + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class delete_issue extends \external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters( + array( + 'issueid' => new external_value(PARAM_INT, 'issue id'), + ) + ); + } + + /** + * Deletes issue with id $issueid + * + * @param int $issueid id of the issue to be deleted + * + * @return array $result containing status, the issueid and any warnings + */ + public static function execute($issueid) { + global $USER, $DB; + + $deleted = false; + $warnings = array(); + + // Parameter validation. + $params = self::validate_parameters(self::execute_parameters(), + array( + 'issueid' => (int) $issueid, + ) + ); + + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :issueid AND userid = :userid', + array( + 'issueid' => $params['issueid'], + 'userid' => $USER->id + ) + )) { + throw new \moodle_exception('cannotdeleteissue', 'local_qtracker', '', $params['issueid']); + } + + $issue = issue::load($params['issueid']); + + // Context validation. + $context = \context::instance_by_id($issue->get_contextid()); + self::validate_context($context); + + // Capability checking. + issue_require_capability_on($issue->get_issue_obj(), 'edit'); + + if (empty($warnings)) { + $deleted = $issue->delete(); + } + + $result = array(); + $result['status'] = $deleted; + $result['issueid'] = $params['issueid']; + $result['warnings'] = $warnings; + + return $result; + } + + /** + * Returns description of method result value + * + * @return external_description + */ + public static function execute_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'issueid' => new external_value(PARAM_INT, 'The id of the new issue'), + 'warnings' => new external_warnings() + ) + ); + } +} diff --git a/classes/external/delete_issue_relation.php b/classes/external/delete_issue_relation.php new file mode 100644 index 0000000..3031d2f --- /dev/null +++ b/classes/external/delete_issue_relation.php @@ -0,0 +1,141 @@ +. + +/** + * External (web service) function calls for retrieving a question issue. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/externallib.php"); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use external_value; +use external_function_parameters; +use external_single_structure; +use external_multiple_structure; +use external_warnings; +use local_qtracker\issue; + +/** + * delete_issue_relation class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class delete_issue_relation extends \external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters( + array( + 'parentid' => new external_value(PARAM_INT, 'issue id of parent'), + 'childid' => new external_value(PARAM_INT, 'issue id of child') + ) + ); + } + + /** + * Supersedes issue passed as child under issue passed as parent + * + * @param int $parentid id of the parent issue to supersede a child issue + * @param int $childid id of the child issue to be superseded by a parent issue + * + * @return array with status, the issuedata, and any warnings + */ + public static function execute($parentid, $childid) { + global $PAGE, $DB; + + $deleted = false; + $warnings = array(); + + // Parameter validation. + $params = self::validate_parameters(self::execute_parameters(), + array( + 'parentid' => (int) $parentid, + 'childid' => (int) $childid, + ) + ); + + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :parentid', + array( + 'parentid' => $params['parentid'] + ) + )) { + throw new \moodle_exception('cannoteditissue', 'local_qtracker', '', $params['parentid']); + } + + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :childid', + array( + 'childid' => $params['childid'] + ) + )) { + throw new \moodle_exception('cannoteditissue', 'local_qtracker', '', $params['childid']); + } + + + $parent = issue::load($params['parentid']); + $child = issue::load($params['childid']); + + // Context validation. + $parentcontext = \context::instance_by_id($parent->get_contextid()); + self::validate_context($parentcontext); + $childcontext = \context::instance_by_id($child->get_contextid()); + self::validate_context($childcontext); + + // Capability checking. + issue_require_capability_on($parent->get_issue_obj(), 'edit'); + issue_require_capability_on($child->get_issue_obj(), 'edit'); + + if (empty($warnings)) { + $deleted = $parent->remove_child($child); + } + + $result = array(); + $result['status'] = $deleted; + $result['warnings'] = $warnings; + + return $result; + } + + /** + * Returns description of get_issues result value. + * + * @return external_description + * @since Moodle 2.5 + */ + public static function execute_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'warnings' => new external_warnings() + ) + ); + } +} diff --git a/classes/external/edit_issue.php b/classes/external/edit_issue.php new file mode 100644 index 0000000..08c343e --- /dev/null +++ b/classes/external/edit_issue.php @@ -0,0 +1,152 @@ +. + +/** + * External (web service) function calls for editing a question issue. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/externallib.php"); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use external_value; +use external_function_parameters; +use moodle_exception; +use external_single_structure; +use external_warnings; +use local_qtracker\issue; + +/** + * edit_issue class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class edit_issue extends \external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters( + array( + 'issueid' => new external_value(PARAM_INT, 'issue id'), + 'issuetitle' => new external_value(PARAM_TEXT, 'issue title'), + 'issuedescription' => new external_value(PARAM_RAW, 'issue description'), + ) + ); + } + + /** + * Edits issue + * + * @param int $issueid id of the issue to be edited + * @param string $issuetitle new issue title + * @param string $issuedescription new issue description + * + * @return array with status, issueid and any warnings + */ + public static function execute($issueid, $issuetitle, $issuedescription) { + global $USER, $DB; + + $added = false; + $warnings = array(); + + // Parameter validation. + $params = self::validate_parameters(self::execute_parameters(), + array( + 'issueid' => (int) $issueid, + 'issuetitle' => $issuetitle, + 'issuedescription' => $issuedescription, + ) + ); + + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :issueid AND userid = :userid', + array( + 'issueid' => $params['issueid'], + 'userid' => $USER->id + ) + )) { + throw new \moodle_exception('cannoteditissue', 'local_qtracker', '', $params['issueid']); + } + + $issue = issue::load($params['issueid']); + + // Context validation. + $context = \context::instance_by_id($issue->get_contextid()); + self::validate_context($context); + + // Capability checking. + issue_require_capability_on($issue->get_issue_obj(), 'edit'); + + if (empty($params['issuetitle'])) { + $warnings[] = array( + 'item' => 'issuetitle', + 'itemid' => 0, + 'warningcode' => 'fielderror', + 'message' => 'Empty issue title.' + ); + } + + if (empty($params['issuedescription'])) { + $warnings[] = array( + 'item' => 'issuedescription', + 'itemid' => 0, + 'warningcode' => 'fielderror', + 'message' => 'Empty issue description.', + ); + } + if (empty($warnings)) { + $issue->set_title($params['issuetitle']); + $issue->set_description($params['issuedescription']); + $added = true; + } + + $result = array(); + $result['status'] = $added; + $result['issueid'] = $params['issueid']; + $result['warnings'] = $warnings; + + return $result; + } + + /** + * Returns description of method result value + * + * @return external_description + */ + public static function execute_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'issueid' => new external_value(PARAM_INT, 'The id of the new issue'), + 'warnings' => new external_warnings() + ) + ); + } +} diff --git a/classes/external/get_issue.php b/classes/external/get_issue.php new file mode 100644 index 0000000..8052563 --- /dev/null +++ b/classes/external/get_issue.php @@ -0,0 +1,139 @@ +. + +/** + * External (web service) function calls for retrieving a question issue. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/externallib.php"); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use external_value; +use external_function_parameters; +use external_single_structure; +use external_warnings; +use local_qtracker\issue; +use local_qtracker\external\helper; + +/** + * get_issue class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_issue extends \external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters( + array( + 'issueid' => new external_value(PARAM_INT, 'issue id') + ) + ); + } + + /** + * Returns issue with the id $issueid + * + * @param int $issueid id of the issue to be returned + * + * @return array with status, the issuedata, and any warnings + */ + public static function execute($issueid) { + global $USER, $DB; + + $status = false; + $issuedata = array(); + $warnings = array(); + + // Parameter validation. + $params = self::validate_parameters(self::execute_parameters(), + array( + 'issueid' => (int) $issueid, + ) + ); + + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :issueid AND userid = :userid', + array( + 'issueid' => $params['issueid'], + 'userid' => $USER->id + ) + )) { + throw new \moodle_exception('cannotgetissue', 'local_qtracker', '', $params['issueid']); + } + + $issue = issue::load($params['issueid']); + + // Context validation. + $context = \context::instance_by_id($issue->get_contextid()); + self::validate_context($context); + + // Capability checking. + issue_require_capability_on($issue->get_issue_obj(), 'view'); + + if (empty($warnings)) { + $issuedata['id'] = $issue->get_id(); + $issuedata['title'] = $issue->get_title(); + $issuedata['description'] = $issue->get_description(); + $issuedata['state'] = $issue->get_state(); + $issuedata['questionid'] = $issue->get_questionid(); + $issuedata['questionusageid'] = $issue->get_qubaid(); + $issuedata['contextid'] = $issue->get_contextid(); + $issuedata['slot'] = $issue->get_slot(); + $issuedata['userid'] = $issue->get_userid(); + $issuedata['timecreated'] = $issue->get_timecreated(); + + $status = true; + } + + $result = array(); + $result['status'] = $status; + $result['issue'] = $issuedata; + $result['warnings'] = $warnings; + + return $result; + } + + /** + * Returns description of method result value + * @return external_description + */ + public static function execute_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'issue' => helper::issue_description(), + 'warnings' => new external_warnings() + ) + ); + + } +} diff --git a/classes/external/get_issue_children.php b/classes/external/get_issue_children.php new file mode 100644 index 0000000..ab0c41c --- /dev/null +++ b/classes/external/get_issue_children.php @@ -0,0 +1,147 @@ +. + +/** + * External (web service) function calls for retrieving a question issue. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/externallib.php"); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use external_value; +use external_function_parameters; +use external_single_structure; +use external_multiple_structure; +use external_warnings; +use local_qtracker\issue; + +/** + * get_issue class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_issue_children extends \external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters( + array( + 'issueid' => new external_value(PARAM_INT, 'issue id') + ) + ); + } + + /** + * Returns children of issue with the id $issueid + * + * @param int $issueid id of the issue to be returned + * + * @return array with status, the issuedata, and any warnings + */ + public static function execute($issueid) { + global $PAGE, $DB; + + $issue = array(); + $children = array(); + $warnings = array(); + + // Parameter validation. + $params = self::validate_parameters(self::execute_parameters(), + array( + 'issueid' => (int) $issueid, + ) + ); + + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :issueid', + array( + 'issueid' => $params['issueid'] + ) + )) { + throw new \moodle_exception('cannotgetissue', 'local_qtracker', '', $params['issueid']); + } + + $issue = issue::load($params['issueid']); + + // Context validation. + $context = \context::instance_by_id($issue->get_contextid()); + self::validate_context($context); + + // Capability checking. + issue_require_capability_on($issue->get_issue_obj(), 'view'); + + + $children = $issue->get_children(); + + $returnedchildren = array(); + foreach ($children as $child) { + // Context validation. + $context = \context::instance_by_id($child->get_contextid()); + self::validate_context($context); + + // Capability checking. + issue_require_capability_on($child->get_id(), 'view'); + + $renderer = $PAGE->get_renderer('core'); + $exporter = new issue_exporter($child->get_issue_obj(), ['context' => $context]); + $childdetails = $exporter->export($renderer); + // Return the issue only if all the searched fields are returned. + // Otherwise it means that the $issue was not allowed to search the returned issue. + if (!empty($childdetails)) { + $validchild = true; + + if ($validchild) { + $returnedchildren[] = $childdetails; + } + } + } + + return array('children' => $returnedchildren, 'warnings' => $warnings); + + } + + /** + * Returns description of get_issues result value. + * + * @return external_description + * @since Moodle 2.5 + */ + public static function execute_returns() { + return new external_single_structure( + array( + 'children' => new external_multiple_structure( + issue_exporter::get_read_structure() + ), + 'warnings' => new external_warnings() + ) + ); + } +} diff --git a/classes/external/get_issue_parents.php b/classes/external/get_issue_parents.php new file mode 100644 index 0000000..a91a5c7 --- /dev/null +++ b/classes/external/get_issue_parents.php @@ -0,0 +1,149 @@ +. + +/** + * External (web service) function calls for retrieving a question issue. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/externallib.php"); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use external_value; +use external_function_parameters; +use external_single_structure; +use external_multiple_structure; +use external_warnings; +use local_qtracker\issue; + +/** + * get_issue class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_issue_parents extends \external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters( + array( + 'issueid' => new external_value(PARAM_INT, 'issue id') + ) + ); + } + + /** + * Returns parents of issue with the id $issueid + * + * @param int $issueid id of the issue to be returned + * + * @return array with status, the issuedata, and any warnings + */ + public static function execute($issueid) { + global $PAGE, $DB; + + $issue = array(); + $parents = array(); + $warnings = array(); + + // Parameter validation. + $params = self::validate_parameters(self::execute_parameters(), + array( + 'issueid' => (int) $issueid, + ) + ); + + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :issueid', + array( + 'issueid' => $params['issueid'] + ) + )) { + throw new \moodle_exception('cannotgetissue', 'local_qtracker', '', $params['issueid']); + } + + + $issue = issue::load($params['issueid']); + + // Context validation. + $context = \context::instance_by_id($issue->get_contextid()); + self::validate_context($context); + + // Capability checking. + issue_require_capability_on($issue->get_issue_obj(), 'view'); + + + $parents = $issue->get_parents(); + + $returnedparents = array(); + foreach ($parents as $parent) { + // Context validation. + + $context = \context::instance_by_id($parent->get_contextid()); + self::validate_context($context); + + // Capability checking. + issue_require_capability_on($parent->get_id(), 'view'); + + $renderer = $PAGE->get_renderer('core'); + $exporter = new issue_exporter($parent->get_issue_obj(), ['context' => $context]); + $parentdetails = $exporter->export($renderer); + // Return the issue only if all the searched fields are returned. + // Otherwise it means that the $issue was not allowed to search the returned issue. + if (!empty($parentdetails)) { + $validparent = true; + + if ($validparent) { + $returnedparents[] = $parentdetails; + } + } + } + + return array('parents' => $returnedparents, 'warnings' => $warnings); + + } + + /** + * Returns description of get_issues result value. + * + * @return external_description + * @since Moodle 2.5 + */ + public static function execute_returns() { + return new external_single_structure( + array( + 'parents' => new external_multiple_structure( + issue_exporter::get_read_structure() + ), + 'warnings' => new external_warnings() + ) + ); + } +} diff --git a/classes/external/get_issues.php b/classes/external/get_issues.php new file mode 100644 index 0000000..5354cf9 --- /dev/null +++ b/classes/external/get_issues.php @@ -0,0 +1,234 @@ +. + +/** + * External (web service) function calls for retrieving a question issue. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/externallib.php"); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use external_value; +use external_function_parameters; +use external_single_structure; +use external_multiple_structure; +use external_warnings; +use local_qtracker\issue; +use local_qtracker\external\helper; +use local_qtracker\external\issue_exporter; + +/** + * get_issues class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_issues extends \external_api { + + /** + * Returns description of get_issues() parameters. + * + * @return external_function_parameters + * @since Moodle 2.5 + */ + public static function execute_parameters() { + return new external_function_parameters( + array( + 'criteria' => new external_multiple_structure( + new external_single_structure( + array( + 'key' => new external_value(PARAM_ALPHA, 'the issue column to search, expected keys (value format) are: + "id" (int) matching issue id, + "questionid" (int) issue questionid, + "state" (string) issue state, + "title" (Sstring) issue title, + (Note: you can use % for searching but it may be considerably slower!)'), + 'value' => new external_value(PARAM_RAW, 'the value to search') + ) + ), + 'the key/value pairs to be considered in issue search. Values can not be empty. + Specify different keys only once (fullname => \'issue1\', auth => \'manual\', ...) - + key occurences are forbidden. + The search is executed with AND operator on the criterias. Invalid criterias (keys) are ignored, + the search is still executed on the valid criterias. + You can search without criteria, but the function is not designed for it. + It could very slow or timeout. The function is designed to search some specific issues.' + ), + 'from' => new external_value(PARAM_INT, 'Start returning records from here', VALUE_DEFAULT, 0), + 'limit' => new external_value(PARAM_INT, 'Number of records to return', VALUE_DEFAULT, 20) + ) + ); + } + + /** + * Retrieve matching issue. + * + * @throws moodle_exception + * @param array $criteria the allowed array keys are id/lastname/firstname/idnumber/issuename/email/auth. + * @return array An array of arrays containing issue profiles. + * @since Moodle 2.5 + */ + public static function execute($criteria = array(), $from, $limit) { + global $CFG, $issue, $DB, $PAGE, $USER; + + require_once($CFG->dirroot . "/local/qtracker/lib.php"); + $params = self::validate_parameters( + self::execute_parameters(), + array( + 'criteria' => $criteria, + 'from' => $from, + 'limit' => $limit,) + ); + + // Validate the criteria and retrieve the issues. + $issues = array(); + $warnings = array(); + $sqlparams = array(); + $usedkeys = array(); + + $sql = '1 = 1'; + foreach ($params['criteria'] as $criteriaindex => $criteria) { + + // Check that the criteria has never been used. + if (array_key_exists($criteria['key'], $usedkeys)) { + throw new moodle_exception('keyalreadyset', '', '', null, 'The key ' . $criteria['key'] . ' can only be sent once'); + } else { + $usedkeys[$criteria['key']] = true; + } + + $invalidcriteria = false; + // Clean the parameters. + $paramtype = PARAM_RAW; + switch ($criteria['key']) { + case 'id': + $paramtype = PARAM_INT; + break; + case 'questionid': + $paramtype = PARAM_INT; + break; + case 'userid': + $paramtype = PARAM_INT; + break; + case 'state': + $paramtype = PARAM_TEXT; + break; + case 'title': + $paramtype = PARAM_TEXT; + break; + default: + // Send back a warning that this search key is not supported in this version. + // This warning will make the function extandable without breaking clients. + $warnings[] = array( + 'item' => $criteria['key'], + 'warningcode' => 'invalidfieldparameter', + 'message' => + 'The search key \'' . $criteria['key'] . '\' is not supported, look at the web service documentation' + ); + // Do not add this invalid criteria to the created SQL request. + $invalidcriteria = true; + unset($params['criteria'][$criteriaindex]); + break; + } + + if (!$invalidcriteria) { + $cleanedvalue = clean_param($criteria['value'], $paramtype); + + $sql .= ' AND '; + + // Create the SQL. + switch ($criteria['key']) { + case 'id': + case 'questionid': + case 'state': + $sql .= $criteria['key'] . ' = :' . $criteria['key']; + $sqlparams[$criteria['key']] = $cleanedvalue; + break; + case 'title': + $sql .= $DB->sql_like($criteria['key'], ':' . $criteria['key'], false); + $sqlparams[$criteria['key']] = $cleanedvalue; + break; + default: + break; + } + } + } + + $limitfrom= $params['from']; + $limitnum= $params['limit']; + $issues = $DB->get_records_select('local_qtracker_issue', $sql, $sqlparams, 'id ASC', '*', $limitfrom, $limitnum); + + // Finally retrieve each issues information. + $returnedissues = array(); + foreach ($issues as $issue) { + // Context validation. + $context = \context::instance_by_id($issue->contextid); + self::validate_context($context); + + // Capability checking. + issue_require_capability_on($issue, 'view'); + + $renderer = $PAGE->get_renderer('core'); + $exporter = new issue_exporter($issue, ['context' => $context]); + $issuedetails = $exporter->export($renderer); + // Return the issue only if all the searched fields are returned. + // Otherwise it means that the $issue was not allowed to search the returned issue. + if (!empty($issuedetails)) { + $validissue = true; + + foreach ($params['criteria'] as $criteria) { + if (empty($issuedetails->{$criteria['key']})) { + $validissue = false; + } + } + + if ($validissue) { + $returnedissues[] = $issuedetails; + } + } + } + + return array('issues' => $returnedissues, 'warnings' => $warnings); + } + + /** + * Returns description of get_issues result value. + * + * @return external_description + * @since Moodle 2.5 + */ + public static function execute_returns() { + return new external_single_structure( + array( + 'issues' => new external_multiple_structure( + issue_exporter::get_read_structure() + ), + 'warnings' => new external_warnings('always set to \'key\'', 'faulty key name') + ) + ); + } +} diff --git a/classes/external/get_question.php b/classes/external/get_question.php new file mode 100644 index 0000000..9e6518c --- /dev/null +++ b/classes/external/get_question.php @@ -0,0 +1,120 @@ +. + +/** + * External (web service) function calls for retrieving a question issue. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/externallib.php"); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use external_value; +use external_function_parameters; +use external_single_structure; +use external_warnings; +use local_qtracker\external\helper; + +/** + * get_question class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_question extends \external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters( + array( + 'id' => new external_value(PARAM_INT, 'question id') + ) + ); + } + + + /** + * Retrieves question + * + * @param int $questionid id of the question to be retrieved + * + * @return array with status, summary of the question and any warnings + */ + public static function execute($questionid) { + global $PAGE, $USER; + + $status = false; + $warnings = array(); + + // Parameter validation. + $params = self::validate_parameters(self::execute_parameters(), + array( + 'id' => (int) $questionid, + ) + ); + + $question = \question_bank::load_question_data($params['id']); + if (!$question) { + throw new \moodle_exception('cannotgetquestion', 'local_qtracker', '', $params['id']); + } + + // Context validation. + $context = \context::instance_by_id($question->contextid); + self::validate_context($context); + + question_require_capability_on($question, 'view'); + + $renderer = $PAGE->get_renderer('core'); + $exporter = new \core_question\external\question_summary_exporter($question, ['context' => $context]); + $questionsummary = $exporter->export($renderer); + + $result = array(); + $result['status'] = $status; + $result['question'] = $questionsummary; + $result['warnings'] = $warnings; + + return $result; + } + + /** + * Returns description of method result value + * @return external_description + */ + public static function execute_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'question' => \core_question\external\question_summary_exporter::get_read_structure(), + 'warnings' => new external_warnings() + ) + ); + + } +} diff --git a/classes/external/helper.php b/classes/external/helper.php new file mode 100644 index 0000000..0657aed --- /dev/null +++ b/classes/external/helper.php @@ -0,0 +1,66 @@ +. + +/** + * External (web service) function calls for retrieving a question issue. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +use external_value; +use external_single_structure; + +/** + * helper class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + /** + * Create issue return value description. + * + * @param array $additionalfields some additional field + * @return single_structure_description + */ + public static function issue_description($additionalfields = array()) { + $issuefields = array( + 'id' => new external_value(PARAM_INT, 'The id of the issue'), + 'title' => new external_value(PARAM_TEXT, 'The issue title.'), + 'description' => new external_value(PARAM_RAW, 'The issue description.'), + 'state' => new external_value(PARAM_TEXT, 'The issue state.'), + 'questionid' => new external_value(PARAM_INT, 'The question id for this issue.'), + 'questionusageid' => new external_value(PARAM_INT, 'The question usage id for this issue.'), + 'contextid' => new external_value(PARAM_INT, 'The context id for this issue.'), + 'slot' => new external_value(PARAM_INT, 'The issslot for the question for the issue.'), + 'userid' => new external_value(PARAM_INT, 'The user id for the user who created the issue.'), + 'timecreated' => new external_value(PARAM_INT, 'The time the issue was created.'), + ); + if (!empty($additionalfields)) { + $issuefields = array_merge($issuefields, $additionalfields); + } + return new external_single_structure($issuefields); + } +} diff --git a/classes/external/issue_comment_exporter.php b/classes/external/issue_comment_exporter.php new file mode 100644 index 0000000..8520ee3 --- /dev/null +++ b/classes/external/issue_comment_exporter.php @@ -0,0 +1,79 @@ +. + +/** + * Exporter for exporting question issue data. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +use \core\external\exporter; +use \renderer_base; + + +/** + * Class for displaying a list of issue comment data. + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issue_comment_exporter extends exporter { + + + /** + * Return the list of additional properties. + * + * @return array + */ + protected static function define_other_properties() { + return [ + 'id' => [ + 'type' => PARAM_INT, + ], + 'description' => [ + 'type' => PARAM_RAW, + ], + 'issueid' => [ + 'type' => PARAM_INT, + ], + 'userid' => [ + 'type' => PARAM_INT, + ], + 'timecreated' => [ + 'type' => PARAM_INT, + ], + ]; + } + + /** + * Return a list of objects that are related + * + * @return array + */ + protected static function define_related() { + return array( + 'context' => 'context', + ); + } +} diff --git a/classes/external/issue_exporter.php b/classes/external/issue_exporter.php new file mode 100644 index 0000000..6a60b6f --- /dev/null +++ b/classes/external/issue_exporter.php @@ -0,0 +1,95 @@ +. + +/** + * Exporter for exporting question issue data. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +use \core\external\exporter; + +/** + * Class for displaying a list of issue data. + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issue_exporter extends exporter { + + + /** + * Return the list of additional properties. + * + * @return array + */ + protected static function define_other_properties() { + return [ + 'id' => [ + 'type' => PARAM_INT, + ], + 'title' => [ + 'type' => PARAM_TEXT, + ], + 'description' => [ + 'type' => PARAM_TEXT, + ], + 'questionid' => [ + 'type' => PARAM_INT, + ], + 'questionusageid' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED, + ], + 'slot' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED, + ], + 'state' => [ + 'type' => PARAM_TEXT, + ], + 'contextid' => [ + 'type' => PARAM_INT, + ], + 'userid' => [ + 'type' => PARAM_INT, + ], + 'timecreated' => [ + 'type' => PARAM_INT, + ], + ]; + } + + + /** + * Return a list of objects that are related + * + * @return array + */ + protected static function define_related() { + return array( + 'context' => 'context', + ); + } +} diff --git a/classes/external/new_issue.php b/classes/external/new_issue.php new file mode 100644 index 0000000..fd9b51c --- /dev/null +++ b/classes/external/new_issue.php @@ -0,0 +1,161 @@ +. + +/** + * External (web service) function calls for creating a new question issue. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/externallib.php"); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use external_value; +use external_function_parameters; +use moodle_exception; +use external_single_structure; +use external_warnings; +use local_qtracker\issue; + +/** + * new_issue class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class new_issue extends \external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters( + array( + 'qubaid' => new external_value(PARAM_INT, 'question usage id'), + 'slot' => new external_value(PARAM_INT, 'slot'), + 'contextid' => new external_value(PARAM_INT, 'issue context'), + 'issuetitle' => new external_value(PARAM_TEXT, 'issue title'), + 'issuedescription' => new external_value(PARAM_TEXT, 'issue description'), + ) + ); + } + + /** + * Creates new issue + * @param int $qubaid new issues quba id + * @param int $slot new issues slot + * @param int $contextid new issues context id + * @param string $issuetitle new issues title + * @param string $issuedescription new issues description + * + * @return array with status, issueid and any warnings + */ + public static function execute($qubaid, $slot, $contextid, $issuetitle, $issuedescription) { + global $USER, $DB; + + $added = false; + $warnings = array(); + + // Parameter validation. + $params = self::validate_parameters(self::execute_parameters(), + array( + 'qubaid' => (int) $qubaid, + 'slot' => (int) $slot, + 'contextid' => (int) $contextid, + 'issuetitle' => $issuetitle, + 'issuedescription' => $issuedescription, + ) + ); + + // Context validation. + // TODO: ensure proper validation.... + $context = \context::instance_by_id($params['contextid']); + self::validate_context($context); + + // Capability checking. + if (!has_capability('local/qtracker:addissue', $context)) { + throw new moodle_exception('cannotcreateissue', 'local_qtracker'); + } + + if (empty($params['issuetitle'])) { + $warnings[] = array( + 'item' => 'issuetitle', + 'itemid' => 0, + 'warningcode' => 'fielderror', + 'message' => 'Empty issue title.' + ); + } + + if (empty($params['issuedescription'])) { + $warnings[] = array( + 'item' => 'issuedescription', + 'itemid' => 0, + 'warningcode' => 'fielderror', + 'message' => 'Empty issue description.', + ); + } + + $quba = \question_engine::load_questions_usage_by_activity($params['qubaid']); + $question = $quba->get_question($params['slot']); + + $issueid = 0; + + if (empty($warnings)) { + $issue = issue::create( + $params['issuetitle'], + $params['issuedescription'], + $question, + $params['contextid'], + $quba, + $params['slot'] + ); + $issueid = $issue->get_id(); + $added = true; + } + + $result = array(); + $result['status'] = $added; + $result['issueid'] = $issueid; + $result['warnings'] = $warnings; + + return $result; + } + + /** + * Returns description of method result value + * @return external_description + */ + public static function execute_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'issueid' => new external_value(PARAM_INT, 'The id of the new issue'), + 'warnings' => new external_warnings() + ) + ); + } +} diff --git a/classes/external/reference_exporter.php b/classes/external/reference_exporter.php new file mode 100644 index 0000000..d3c9936 --- /dev/null +++ b/classes/external/reference_exporter.php @@ -0,0 +1,75 @@ +. + +/** + * Exporter for exporting question issue data. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +use \core\external\exporter; + + +/** + * Class for displaying a list of issue comment data. + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reference_exporter extends exporter { + + + /** + * Return the list of additional properties. + * + * @return array + */ + protected static function define_other_properties() { + return [ + 'id' => [ + 'type' => PARAM_INT, + ], + 'sourceid' => [ + 'type' => PARAM_INT, + ], + 'targetid' => [ + 'type' => PARAM_INT, + ], + 'reftype' => [ + 'type' => PARAM_RAW, + ] + ]; + } + + /** + * Return a list of objects that are related + * + * @return array + */ + protected static function define_related() { + return array( + 'context' => 'context', + ); + } +} diff --git a/classes/external/set_issue_relation.php b/classes/external/set_issue_relation.php new file mode 100644 index 0000000..0896816 --- /dev/null +++ b/classes/external/set_issue_relation.php @@ -0,0 +1,141 @@ +. + +/** + * External (web service) function calls for retrieving a question issue. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/externallib.php"); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use external_value; +use external_function_parameters; +use external_single_structure; +use external_multiple_structure; +use external_warnings; +use local_qtracker\issue; + +/** + * set_issue_relation class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class set_issue_relation extends \external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters( + array( + 'parentid' => new external_value(PARAM_INT, 'issue id of parent'), + 'childid' => new external_value(PARAM_INT, 'issue id of child') + ) + ); + } + + /** + * Supersedes issue passed as child under issue passed as parent + * + * @param int $parentid id of the parent issue to supersede a child issue + * @param int $childid id of the child issue to be superseded by a parent issue + * + * @return array with status, the issuedata, and any warnings + */ + public static function execute($parentid, $childid) { + global $PAGE, $DB; + + $added = false; + $warnings = array(); + + // Parameter validation. + $params = self::validate_parameters(self::execute_parameters(), + array( + 'parentid' => (int) $parentid, + 'childid' => (int) $childid, + ) + ); + + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :parentid', + array( + 'parentid' => $params['parentid'] + ) + )) { + throw new \moodle_exception('cannoteditissue', 'local_qtracker', '', $params['parentid']); + } + + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :childid', + array( + 'childid' => $params['childid'] + ) + )) { + throw new \moodle_exception('cannoteditissue', 'local_qtracker', '', $params['childid']); + } + + + $parent = issue::load($params['parentid']); + $child = issue::load($params['childid']); + + // Context validation. + $parentcontext = \context::instance_by_id($parent->get_contextid()); + self::validate_context($parentcontext); + $childcontext = \context::instance_by_id($child->get_contextid()); + self::validate_context($childcontext); + + // Capability checking. + issue_require_capability_on($parent->get_issue_obj(), 'edit'); + issue_require_capability_on($child->get_issue_obj(), 'edit'); + + if (empty($warnings)) { + $added = $parent->supersede_issue($child); + } + + $result = array(); + $result['status'] = $added; + $result['warnings'] = $warnings; + + return $result; + } + + /** + * Returns description of get_issues result value. + * + * @return external_description + * @since Moodle 2.5 + */ + public static function execute_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'warnings' => new external_warnings() + ) + ); + } +} diff --git a/classes/form/view/new_issue_form.php b/classes/form/view/new_issue_form.php new file mode 100644 index 0000000..12a0434 --- /dev/null +++ b/classes/form/view/new_issue_form.php @@ -0,0 +1,87 @@ +. + +/** + * Question form + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\form\view; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->libdir . '/questionlib.php'); + +/** + * Question form + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class new_issue_form extends \moodleform { + + /** + * question_details_form constructor. + * @param \stdClass $question The question to be formed + * @param \moodle_url $url Questions moodle url + */ + public function __construct(\moodle_url $url) { + parent::__construct($url); + } + + + /** + * Defines form + */ + public function definition() { + $mform = $this->_form; + $mform->addElement('header', 'generalheader', get_string("general", 'form')); + $mform->addElement('text', 'title', get_string('title', 'local_qtracker')); + $mform->addRule('title', get_string('required'), 'required', null, 'client'); + $mform->setType('title', PARAM_RAW); + + $mform->addElement('text', 'questionid', get_string('questionid', 'local_qtracker')); + $mform->addRule('questionid', get_string('required'), 'required', null, 'client'); + $mform->addRule('questionid', get_string('required'), 'nonzero', null, 'client'); + $mform->setType('questionid', PARAM_INT); + + $mform->addElement('editor', 'description', get_string('description', 'local_qtracker')); + $mform->addRule('description', get_string('required'), 'required', null, 'client'); + $mform->setType('description', PARAM_RAW); + + $this->add_action_buttons(); + } + + function validation($data, $files) { + global $DB; + $errors = parent::validation($data, $files); + $questionid = $data['questionid']; + $question = $DB->get_record('question', array('id' => $questionid)); + if (!$question) { + $errors['questionid'] = get_string('errornonexistingquestion', 'local_qtracker'); + } else { + question_require_capability_on($question, 'use'); + } + + return $errors; + } +} diff --git a/classes/form/view/question_details_form.php b/classes/form/view/question_details_form.php new file mode 100644 index 0000000..48d3299 --- /dev/null +++ b/classes/form/view/question_details_form.php @@ -0,0 +1,70 @@ +. + +/** + * Question form + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\form\view; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); + +/** + * Question form + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_details_form extends \moodleform { + + /** + * question_details_form constructor. + * @param \stdClass $question The question to be formed + * @param \moodle_url $url Questions moodle url + */ + public function __construct($question, \moodle_url $url) { + $this->question = $question; + parent::__construct($url); + } + + + /** + * Defines form + */ + public function definition() { + $mform = $this->_form; + + $mform->addElement('header', 'question' , + get_string('question', 'core') . " - " . $this->question->name); + + $description = \html_writer::start_div(); + $description .= $this->question->name; + $description .= $this->question->questiontext; + $description .= \html_writer::end_div(); + + $mform->addElement('html', $description); + $mform->setExpanded('question', false); + + } +} diff --git a/classes/issue.php b/classes/issue.php new file mode 100644 index 0000000..cf19981 --- /dev/null +++ b/classes/issue.php @@ -0,0 +1,430 @@ +. + +/** + * Issue class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use local_qtracker\referable; + +/** + * Question issue class. + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issue extends referable { + + /** + * @var \stdClass + */ + protected $issue = null; + + /** + * @var array + */ + protected $comments = array(); + + /** + * Constructor. + * + * @param int|\stdClass $issue + * @return void + */ + public function __construct($issue) { + global $DB; + if (is_scalar($issue)) { + $issue = $DB->get_record('local_qtracker_issue', array('id' => $issue), '*', MUST_EXIST); + if (!$issue) { + throw new \moodle_exception('errorunexistingmodel', 'analytics', '', $issue); + } + } + $this->issue = $issue; + } + + /** + * Returns the issue id. + * + * @return int + */ + public function get_id() { + return $this->issue->id; + } + + /** + * Returns the issue title. + * + * @return int + */ + public function get_title() { + return $this->issue->title; + } + + /** + * Returns the issue description. + * + * @return int + */ + public function get_description() { + return $this->issue->description; + } + + /** + * Returns the issue description. + * + * @return int + */ + public function get_state() { + return $this->issue->state; + } + + /** + * Returns the issue questionusageid. + * + * @return int + */ + public function get_qubaid() { + return $this->issue->questionusageid; + } + + /** + * Returns the issue questionid. + * + * @return int + */ + public function get_questionid() { + return $this->issue->questionid; + } + + /** + * Returns the issue slot. + * + * @return int + */ + public function get_slot() { + return $this->issue->slot; + } + + /** + * Returns the issue contextid. + * + * @return int + */ + public function get_contextid() { + return $this->issue->contextid; + } + + /** + * Returns the issue userid. + * + * @return int + */ + public function get_userid() { + return $this->issue->userid; + } + + /** + * Returns the issue timecreated. + * + * @return int + */ + public function get_timecreated() { + return $this->issue->timecreated; + } + + /** + * Returns a plain \stdClass with the issue data. + * + * @return \stdClass + */ + public function get_issue_obj() { + return $this->issue; + } + + /** + * Returns a plain \stdClass with the issue data. + * + * @param string $description + * + * @return issue_comment + */ + public function create_comment($description) { + $comment = issue_comment::create($description, $this); + array_push($this->comments, $comment); + return $comment; + } + + /** + * Add a new comment to this issue. + * + * @return \stdClass + */ + public function get_comments() { + global $DB; + if (empty($this->comments)) { + $this->comments = array(); + $comments = $DB->get_records('local_qtracker_comment', ['issueid' => $this->get_id()]); + foreach ($comments as $comment) { + array_push($this->comments, new issue_comment($comment)); + } + } + return $this->comments; + } + + /** + * Subsube this issue under another "parent" issue. + * Read: "This issue is superseded by issue passed as param." + * @param issue $issue to subsume under + */ + public function subsume(issue $issue) { + if ($this->is_superseded()) { + return false; + } + $this->make_outgoing_reference($issue, LOCAL_QTRACKER_REFERENCE_SUPERSEDED); + return true; + } + + /** + * Supersede another issue. + * Read: "Issue passed as param is superseded by this issue." + * @param string $description + * + * @return bool Returns true if success, false otherwise + */ + public function supersede_issue(issue $issue) { + if ($issue->is_superseded()) { + return false; + } + $this->make_incoming_reference($issue, LOCAL_QTRACKER_REFERENCE_SUPERSEDED); + $issue->close(); + return true; + } + + /** + * Returns true if issue is superseded by any issue. + */ + public function is_superseded() { + $outrefs = $this->get_outgoing_references(); + $refs = reference_manager::filter_references_by_type($outrefs, LOCAL_QTRACKER_REFERENCE_SUPERSEDED); + if (!empty($refs)) { + return true; + } + return false; + } + + /** + * Get all parent issues (issues that this issue is superseded by) + */ + public function get_parents() { + $parents = []; + $outrefs = $this->get_outgoing_references(); + $refs = reference_manager::filter_references_by_type($outrefs, LOCAL_QTRACKER_REFERENCE_SUPERSEDED); + foreach ($refs as $ref) { + $issue = issue::load($ref->get_target_id()); + array_push($parents, $issue); + } + return $parents; + } + + /** + * Get all child issues (that have been subsumed under this issue) + */ + public function get_children() { + $children = []; + $inrefs = $this->get_incoming_references(); + $refs = reference_manager::filter_references_by_type($inrefs, LOCAL_QTRACKER_REFERENCE_SUPERSEDED); + foreach ($refs as $ref) { + array_push($children, issue::load($ref->get_source_id())); + } + return $children; + } + + /** + * Remove parent issue (that supersedes this issue) + */ + public function remove_parent(issue $parent) { + $parentref = null; + $outrefs = $this->get_outgoing_references(); + $refs = reference_manager::filter_references_by_type($outrefs, LOCAL_QTRACKER_REFERENCE_SUPERSEDED); + foreach ($refs as $ref) { + if ($ref->get_target_id() === $parent->get_id()) { + $parentref = $ref; + } + } + if (is_null($parentref)) { + return false; + } + return $parentref->delete(); + } + + /** + * Remove child issue (that has been subsumed under this issue) + */ + public function remove_child(issue $child) { + $childref = null; + $inrefs = $this->get_incoming_references(); + $refs = reference_manager::filter_references_by_type($inrefs, LOCAL_QTRACKER_REFERENCE_SUPERSEDED); + foreach ($refs as $ref) { + if ($ref->get_source_id() === $child->get_id()) { + $childref = $ref; + } + } + if (is_null($childref)) { + return false; + } + return $childref->delete(); + } + + /** + * Loads and returns issue with id $issueid + * + * @param int $issueid + * @return issue|null + */ + public static function load(int $issueid) { + global $DB; + $issueobj = $DB->get_record('local_qtracker_issue', ['id' => $issueid]); + if ($issueobj === false) { + return null; + } + return new issue($issueobj); + } + + /** + * Creates a new issue. + * + * @param string $title + * @param string $description + * @param \question_definition $question + * @param int $contextid + * @param \question_usage_by_activity|null $quba + * @param int|null $slot + * + * @return issue + */ + public static function create($title, $description, \question_definition $question, $contextid, $quba = null, $slot = null) { + global $USER, $DB; + + $issueobj = new \stdClass(); + $issueobj->title = $title; + $issueobj->description = $description; + $issueobj->questionid = $question->id; + $issueobj->questionusageid = !is_null($quba) ? $quba->get_id() : null; + $issueobj->slot = $slot; + $issueobj->contextid = $contextid; + $issueobj->state = 'new'; + $issueobj->userid = $USER->id; + $time = time(); + $issueobj->timecreated = $time; + // $issueobj->timemodified = $time; + // $issueobj->usermodified = $USER->id; + + $id = $DB->insert_record('local_qtracker_issue', $issueobj); + $issueobj->id = $id; + + $issue = new issue($issueobj); + return $issue; + } + + /** + * Delete this issue. + * + * @return void + */ + public function close() { + global $DB; + $this->issue->state = "closed"; + $DB->update_record('local_qtracker_issue', $this->issue); + } + + /** + * Delete this issue. + * + * @return void + */ + public function open() { + global $DB; + $this->issue->state = "open"; + $DB->update_record('local_qtracker_issue', $this->issue); + } + + /** + * Delete this issue. + * + * @return void + */ + public function comment() { + + $this->comments; + $DB->update_record('local_qtracker_issue', $this->issue); + } + + /** + * Delete this issue and related comments. + * + * @return void + */ + public function delete() { + global $DB; + $comments = $this->get_comments(); + foreach ($comments as $comment) { + $comment->delete(); + } + $outrefs = $this->get_outgoing_references(); + foreach ($outrefs as $outref) { + $outref->delete(); + } + $inrefs = $this->get_incoming_references(); + foreach ($inrefs as $inref) { + $inref->delete(); + } + return $DB->delete_records('local_qtracker_issue', array('id' => $this->get_id())); + } + + /** + * Sets this issues title to $title + * + * @param string $title + */ + public function set_title($title) { + global $DB; + $this->issue->title = $title; + $DB->update_record('local_qtracker_issue', $this->issue); + } + + /** + * Sets this issues description to $title + * + * @param string $title + */ + public function set_description($title) { + global $DB; + $this->issue->description = $title; + $DB->update_record('local_qtracker_issue', $this->issue); + } +} diff --git a/classes/issue_comment.php b/classes/issue_comment.php new file mode 100644 index 0000000..1e44e0f --- /dev/null +++ b/classes/issue_comment.php @@ -0,0 +1,210 @@ +. + +/** + * Issue comment class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Question comment class. + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issue_comment { + + /** + * @var \stdClass + */ + protected $comment = null; + + /** + * Constructor. + * + * @param int|\stdClass $comment + * @return void + */ + public function __construct($comment) { + global $DB; + if (is_scalar($comment)) { + $comment = $DB->get_record('local_qtracker_comment', array('id' => $comment), '*', MUST_EXIST); + if (!$comment) { + throw new \moodle_exception('errorunexistingmodel', 'analytics', '', $comment); + } + } + $this->comment = $comment; + } + + /** + * Returns the comment id. + * + * @return int + */ + public function get_id() { + return $this->comment->id; + } + + /** + * Returns the related issue id. + * + * @return int + */ + public function get_issue_id() { + return $this->comment->issueid; + } + + /** + * Returns the comment description. + * + * @return int + */ + public function get_description() { + return $this->comment->description; + } + + /** + * Returns the comment userid. + * + * @return int + */ + public function get_userid() { + return $this->comment->userid; + } + + /** + * Returns the comment timecreated. + * + * @return int + */ + public function get_timecreated() { + return $this->comment->timecreated; + } + + /** + * Returns true if the comment is mailed to the issue reporter, otherwise false. + * + * @return \stdClass + */ + public function is_mailed() { + return $this->comment->mailed; + } + + /** + * Returns a plain \stdClass with the comment data. + * + * @return \stdClass + */ + public function get_comment_obj() { + return $this->comment; + } + + /** + * Returns a plain \stdClass with the comment data. + * + * @return \stdClass + */ + public function get_comments() { + global $DB; + + return $this->comment; + } + + /** + * Loads and returns issue_comment with id $comment + * + * @param int $comment + * + * @return issue_comment + */ + public static function load(int $comment) { + global $DB; + $commentobj = $DB->get_record('local_qtracker_comment', ['id' => $comment]); + if ($commentobj === false) { + return null; + } + return new issue_comment($commentobj); + } + + /** + * Creates a new comment. + * + * @param string $description + * @param issue $issue + * + * @return issue_comment + */ + public static function create($description, issue $issue) { + global $USER, $DB; + + $commentobj = new \stdClass(); + $commentobj->description = $description; + $commentobj->issueid = $issue->get_id(); + $commentobj->userid = $USER->id; + $time = time(); + $commentobj->timecreated = $time; + // $commentobj->usermodified = $USER->id; + + $id = $DB->insert_record('local_qtracker_comment', $commentobj); + $commentobj->id = $id; + + $comment = new issue_comment($commentobj); + return $comment; + } + + /** + * Delete this comment. + * + * @return void + */ + public function delete() { + global $DB; + return $DB->delete_records('local_qtracker_comment', array('id' => $this->get_id())); + } + + /** + * Sets description of this comment + * + * @param string $title + * + * @return void + */ + public function set_description($title) { + global $DB; + $this->comment->description = $title; + $DB->update_record('local_qtracker_comment', $this->comment); + } + + /** + * Marks the commennt as mailed to the issue reporter + * + * @return \stdClass + */ + public function mark_mailed() { + global $DB; + $this->comment->mailed = true; + $DB->update_record('local_qtracker_comment', $this->comment); + } +} diff --git a/classes/output/email/issue_comment_email.php b/classes/output/email/issue_comment_email.php new file mode 100644 index 0000000..6caee22 --- /dev/null +++ b/classes/output/email/issue_comment_email.php @@ -0,0 +1,305 @@ +. + +/** + * Issue comment email renderable. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\output\email; + +defined('MOODLE_INTERNAL') || die(); + +use local_qtracker\issue; +use renderable; +use templatable; + +/** + * Issue comment email renderable. + * + * @package local_qtracker + * @copyright 2021 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issue_comment_email implements renderable, templatable { + + /** + * The course that the comment is in. + * + * @var object $course + */ + protected $course = null; + + /** + * The comment being displayed. + * + * @var object $comment + */ + protected $comment = null; + + /** + * Whether to override display when displaying usernames. + * @var boolean $viewfullnames + */ + protected $viewfullnames = false; + + /** + * The user that is viewing the comment. + * + * @var object $userto + */ + protected $userto = null; + + /** + * The user that made the comment. + * + * @var object $author + */ + protected $author = null; + + + /** + * Builds a renderable comment email + * + * @param object $course Course of the issue + * @param object $comment Issue comment + * @param object $author Author of the comment + * @param object $recipient Recipient of the email + * @param bool $canreply True if the user can reply to the post + */ + public function __construct($course, $comment, $author, $recipient) { + $this->course = $course; + $this->comment = $comment; + $this->author = $author; + $this->userto = $recipient; + $this->issue = issue::load($this->comment->get_issue_id()); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \mod_forum_renderer $renderer The render to be used for formatting the message + * @param bool $plaintext Whethe the target is a plaintext target + * @return array Data ready for use in a mustache template + */ + public function export_for_template(\renderer_base $renderer, $plaintext = false) { + if ($plaintext) { + return $this->export_for_template_text($renderer); + } else { + return $this->export_for_template_html($renderer); + } + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \mod_forum_renderer $renderer The render to be used for formatting the message and attachments + * @return array Data ready for use in a mustache template + */ + protected function export_for_template_text(\renderer_base $renderer) { + $data = $this->export_for_template_shared($renderer); + return $data + array( + 'id' => html_entity_decode($this->comment->get_id()), + 'coursename' => html_entity_decode($this->get_coursename()), + 'courselink' => html_entity_decode($this->get_courselink()), + 'issuetitle' => html_entity_decode($this->get_issuetitle()), + 'issuedescription' => html_entity_decode($this->get_issuedescription()), + 'authorfullname' => html_entity_decode($this->get_author_fullname()), + 'commentdate' => html_entity_decode($this->get_commentdate()), + 'commentdescription' => html_entity_decode($this->get_commentdescription()), + + // Format some components according to the renderer. + 'message' => html_entity_decode($this->comment->get_description()), + 'authorlink' => $this->get_authorlink(), + 'authorpicture' => $this->get_author_picture($renderer), + ); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $renderer The render to be used for formatting the message and attachments + * @return array Data ready for use in a mustache template + */ + protected function export_for_template_html(\renderer_base $renderer) { + $data = $this->export_for_template_shared($renderer); + return $data + array( + 'id' => $this->comment->get_id(), + 'coursename' => $this->get_coursename(), + 'courselink' => $this->get_courselink(), + 'issuetitle' => $this->get_issuetitle(), + 'issuedescription' => $this->get_issuedescription(), + 'commentdescription' => $this->get_commentdescription(), + 'authorfullname' => $this->get_author_fullname(), + 'commentdate' => $this->get_commentdate(), + + // Format some components according to the renderer. + 'message' => $this->comment->get_description(), + ); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $renderer The render to be used for formatting the message and attachments + * @return stdClass Data ready for use in a mustache template + */ + protected function export_for_template_shared(\renderer_base $renderer) { + return array( + 'issuelink' => $this->get_issuelink(), + 'authorlink' => $this->get_authorlink(), + 'authorpicture' => $this->get_author_picture($renderer), + ); + } + + /** + * Get the link to the course. + * + * @return string + */ + public function get_courselink() { + $link = new \moodle_url( + '/course/view.php', array( + 'id' => $this->course->id, + ) + ); + + return $link->out(false); + } + + /** + * Get the link to the author's profile page. + * + * @return string + */ + public function get_authorlink() { + $link = new \moodle_url( + '/user/view.php', array( + 'id' => $this->comment->get_userid(), + 'course' => $this->course->id, + ) + ); + + return $link->out(false); + } + + /** + * Get the link to reply to the current issue. + * + * @return string + */ + public function get_issuelink() { + return new \moodle_url( + '/local_qtracker/issue.php', array( + 'courseid' => $this->course->id, + 'issueid' => $this->comment->get_issue_id(), + ) + ); + } + + /** + * ID number of the course that the comment is in. + * + * @return string + */ + public function get_courseidnumber() { + return s($this->course->idnumber); + } + + /** + * The full name of the course that the comment is in. + * + * @return string + */ + public function get_coursefullname() { + return format_string($this->course->fullname, true, array( + 'context' => \context_course::instance($this->course->id), + )); + } + + /** + * The name of the course that the comment is in. + * + * @return string + */ + public function get_coursename() { + return format_string($this->course->shortname, true, array( + 'context' => \context_course::instance($this->course->id), + )); + } + + /** + * The title of the current issue. + * + * @return string + */ + public function get_issuetitle() { + return format_string($this->issue->get_title(), true); + } + + /** + * The description of the current issue. + * + * @return string + */ + public function get_issuedescription() { + return format_string($this->issue->get_description(), true); + } + + /** + * The description of the current comment. + * + * @return string + */ + public function get_commentdescription() { + return format_string($this->comment->get_description(), true); + } + + /** + * The date of the comment, formatted according to the comment author's + * preferences. + * + * @return string. + */ + public function get_commentdate() { + global $CFG; + $commentcreated = $this->comment->get_timecreated(); + return userdate($commentcreated, "", \core_date::get_user_timezone($this->comment->get_userid())); + } + + /** + * The fullname of the comment author. + * + * @return string + */ + public function get_author_fullname() { + return fullname($this->author, $this->viewfullnames); + } + + /** + * The HTML for the author's user picture. + * + * @param \renderer_base $renderer + * @return string + */ + public function get_author_picture(\renderer_base $renderer) { + return $renderer->user_picture($this->author, array('courseid' => $this->course->id)); + } +} diff --git a/classes/output/email/renderer.php b/classes/output/email/renderer.php new file mode 100644 index 0000000..01529d9 --- /dev/null +++ b/classes/output/email/renderer.php @@ -0,0 +1,50 @@ +. + +/** + * Email as html renderer. + * + * @package message_email + * @copyright 2019 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\output\email; + +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; +use templatable; + +/** + * Email as html renderer. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends \local_qtracker\output\renderer { + + /** + * The template name for this renderer. + * + * @return string + */ + public function get_template_name() { + return 'email_comment_html'; + } +} diff --git a/classes/output/email/renderer_textemail.php b/classes/output/email/renderer_textemail.php new file mode 100644 index 0000000..093ae41 --- /dev/null +++ b/classes/output/email/renderer_textemail.php @@ -0,0 +1,47 @@ +. + +/** + * Email as text renderer. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\output\email; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Email as text renderer. + * + * @package local_qtracker + * @copyright 2021 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer_textemail extends \local_qtracker\output\email\renderer { + + /** + * The template name for this renderer. + * + * @return string + */ + public function get_template_name() { + return 'email_comment_text'; + } +} diff --git a/classes/output/issue_registration_block.php b/classes/output/issue_registration_block.php new file mode 100644 index 0000000..33c15cd --- /dev/null +++ b/classes/output/issue_registration_block.php @@ -0,0 +1,149 @@ +. + +/** + * Renderable for block + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\output; + +defined('MOODLE_INTERNAL') || die; + +use renderable; +use renderer_base; +use templatable; +use stdClass; +use help_icon; + +/** + * Question issue registration block class. + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issue_registration_block implements renderable, templatable { + + /** @var \question_definition[] Array of {@link https://docs.moodle.org/dev/Question_types} */ + public $questions = array(); + + /** @var \question_usage_by_activity */ + protected $quba; + + /** @var array of stdclass strings to display */ + public $slots = array(); + + /** @var array of existing issue ids display */ + public $issueids = array(); + + /** @var help_icon The help icon. */ + protected $helpicon; + + // TODO: create an alternative (class) for registering issues that are not linked to an attempt.... + + + /** + * Construct the contents of the block + * @param \question_usage_by_activity $quba + * @param stdClass[] $slots + * @param int $contextid + */ + public function __construct(\question_usage_by_activity $quba, $slots, $contextid) { + + $this->quba = $quba; + $this->slots = $slots; + $this->contextid = $contextid; + + // Todo remove questions..... + foreach ($this->slots as $slot) { + $this->questions[] = $this->quba->get_question($slot); + } + $this->load_issues(); + $this->helpicon = new help_icon('question', 'local_qtracker'); + } + + /** + * Loads issues + * + * @return void + */ + private function load_issues() { + global $DB; + + $queryparams = ['questionusageid' => $this->quba->get_id()]; + list($sql, $params) = $DB->get_in_or_equal($this->slots, SQL_PARAMS_NAMED); + $queryparams += $params; + $where = 'questionusageid = :questionusageid AND slot ' . $sql; + $this->issueids = $DB->get_fieldset_select('local_qtracker_issue', 'id', $where, $queryparams); + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass Data to be used for the template + */ + public function export_for_template(renderer_base $output) { + global $PAGE; + $url = $PAGE->url; + $data = new stdClass(); + + // TODO: only check if questions exists... otherwise i dont need them... + if (count($this->questions) > 1) { + $data->hasmultiple = true; + + $select = new stdClass(); + $options = array(); + $select->name = "slot";; + $select->label = "Question"; + $select->helpicon = $this->helpicon->export_for_template($output); + + foreach ($this->questions as $key => $question) { + $option = new stdClass(); + $option->value = $this->slots[$key]; + $option->name = $this->slots[$key]; + array_push($options, $option); + } + $select->options = $options; + $data->select = $select; + } else { + $data->hasmultiple = false; + $data->slot = $this->slots[0]; + } + + $data->qubaid = $this->quba->get_id(); + $data->action = $url; + $data->tooltip = "This is a tooltip"; + + $button = new stdClass(); + $button->type = "submit"; + $button->classes = "col-auto"; + $button->label = "Submit new issue"; + $data->button = $button; + $data->issueids = json_encode($this->issueids); + $data->contextid = $this->contextid; + + // TODO: Fix this as both the button and the select gets this. Wrap in separate mustashe templates. + + // $data->questions = $questions; + return $data; + } +} diff --git a/classes/output/question_issue_page.php b/classes/output/question_issue_page.php new file mode 100644 index 0000000..95b3070 --- /dev/null +++ b/classes/output/question_issue_page.php @@ -0,0 +1,264 @@ +. + +/** + * Renderable for issues page + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/questionlib.php'); + +use coding_exception; +use dml_exception; +use moodle_exception; +use renderable; +use renderer_base; +use stdClass; +use templatable; +use local_qtracker\issue; +use local_qtracker\external\issue_exporter; +use local_qtracker\external\issue_comment_exporter; +use local_qtracker\external\reference_exporter; +use local_qtracker\form\view\question_details_form; +use local_qtracker\reference_manager; + +/** + * Class containing data for question issue page. + * + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_issue_page implements renderable, templatable { + + /** The default number of results to be shown per page. */ + const DEFAULT_PAGE_SIZE = 20; + + /** @var issue|null */ + protected $questionissue = null; + + /** @var array */ + protected $courseid = []; + + /** + * Construct this renderable. + * + * @param issue $questionissue + * @param int $courseid + */ + public function __construct(issue $questionissue, $courseid) { + $this->questionissue = $questionissue; + $this->courseid = $courseid; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output render base + * @return stdClass $data containing the questions data + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + */ + public function export_for_template(renderer_base $output) { + global $USER, $DB, $PAGE; + $data = new stdClass(); + + $context = \context_course::instance($this->courseid); + $issueexporter = new issue_exporter($this->questionissue->get_issue_obj(), ['context' => $context]); + $issuedetails = $issueexporter->export($output); + + // Process default issue description. + $issuedescription = new stdClass(); + $user = $DB->get_record('user', array('id' => $issuedetails->userid)); + $issuedescription->fullname = $user->username; + $userurl = new \moodle_url('/user/view.php'); + $userurl->param('id', $user->id); + $userurl->param('course', $this->courseid); + $issuedescription->userurl = $userurl; + $userpicture = new \user_picture($user); + $userpicture->size = 0; // Size f2. + $issuedescription->profileimageurl = $userpicture->get_url($PAGE)->out(false); + + $issuedetails->issuedescription = $issuedescription; + + $commentsdetails = array(); + + // Process all issue comments... + $comments = $this->questionissue->get_comments(); + foreach ($comments as $comment) { + $commentexporter = new issue_comment_exporter($comment->get_comment_obj(), ['context' => $context]); + $commentdetails = $commentexporter->export($output); + // Get the user data. + $user = $DB->get_record('user', array('id' => $commentdetails->userid)); + $commentdetails->fullname = $user->username; + $userurl = new \moodle_url('/user/view.php'); + $userurl->param('id', $user->id); + $userurl->param('course', $this->courseid); + $commentdetails->userurl = $userurl; + $userpicture = new \user_picture($user); + $userpicture->size = 0; // Size f2. + $commentdetails->profileimageurl = $userpicture->get_url($PAGE)->out(false); + $commentdetails->mailed = $comment->is_mailed(); +/* + $deleteurl = new \moodle_url('/local/qtracker/issue.php'); + $deleteurl->param('courseid', $this->courseid); + $deleteurl->param('issueid', $this->questionissue->get_id()); + $deleteurl->param('deletecommentid', $commentdetails->id); + $commentdetails->deleteurl = $deleteurl; */ + array_push($commentsdetails, $commentdetails); + } + + $issuedetails->comments = $commentsdetails; + $issuedetails->{$issuedetails->state} = true; + $data->questionissue = $issuedetails; + + $data->sendmessage = false; + + $issueurl = new \moodle_url('/local/qtracker/issue.php'); + $issueurl->param('courseid', $this->courseid); + $issueurl->param('issueid', $this->questionissue->get_id()); + $data->action = $PAGE->url->out(false); + + + // Set the user picture data. + $user = $DB->get_record('user', array('id' => $USER->id)); + $userpicture = new \user_picture($user); + $userpicture->size = 0; // Size f2. + $data->profileimageurl = $userpicture->get_url($PAGE)->out(false); + + if ($this->questionissue->get_state() == "closed") { + $reopenbutton = new stdClass(); + $reopenbutton->label = get_string('reopenissue', 'local_qtracker'); + $reopenbutton->name = "reopenissue"; + $reopenbutton->value = true; + $data->reopenbutton = $reopenbutton; + } else { + $closebutton = new stdClass(); + $closebutton->label = get_string('closeissue', 'local_qtracker'); + $closebutton->name = "closeissue"; + $closebutton->value = true; + $data->closebutton = $closebutton; + } + + $triggericon = new stdClass(); + $triggericon->key = "fa-cog"; + $triggericon->title = "Options"; + $triggericon->alt = "Show options"; + $triggericon->extraclasses = ""; + $triggericon->unmappedIcon = false; + + $linkedissues = new stdClass(); + $linkedissues->id = 'linkedissues'; + $linkedissues->search = true; + $linkedissues->label = get_string('subsumedissues', 'local_qtracker'); + $linkedissues->header = get_string('subsumedescription', 'local_qtracker'); + //$linkedissues->items = $links; + $linkedissues->trigger = $triggericon; + + /*$tags = new stdClass(); + $tags->id = 'tags'; + $tags->label = get_string('tags', 'local_qtracker'); + $tags->header = get_string('tagsdescription', 'local_qtracker'); + $tags->items = [$simtag, $simtag2, $simtag3, $simtag4, ]; + $tags->trigger = $triggericon; + $asideblocks = [$linkedissues, $tags]; + */ + + $asideblocks = [$linkedissues]; + $data->asideblocks = $asideblocks; + + + /// + $commentanddmbutton = new stdClass(); + $commentanddmbutton->primary = false; + $commentanddmbutton->name = "commentanddmissue"; + $commentanddmbutton->value = true; + $commentanddmbutton->label = get_string('commentanddm', 'local_qtracker'); + $data->commentanddmbutton = $commentanddmbutton; + + $commentandmailbutton = new stdClass(); + $commentandmailbutton->primary = true; + $commentandmailbutton->name = "commentandmailissue"; + $commentandmailbutton->value = true; + $commentandmailbutton->label = get_string('commentandmail', 'local_qtracker'); + $data->commentandmailbutton = $commentandmailbutton; + + $commentbutton = new stdClass(); + $commentbutton->primary = true; + $commentbutton->name = "commentissue"; + $commentbutton->value = true; + $commentbutton->label = get_string('comment', 'local_qtracker'); + $data->commentbutton = $commentbutton; + +/// + $edittitlebutton = new stdClass(); + $edittitlebutton->primary = true; + $edittitlebutton->name = "edittitle"; + $edittitlebutton->classes = "pr-2 edittitle"; + $edittitlebutton->value = true; + $edittitlebutton->label = get_string('edit', 'local_qtracker'); + $data->edittitlebutton = $edittitlebutton; + + + $newissuebutton = new stdClass(); + $newissuebutton->primary = true; + $newissuebutton->name = "newissue"; + $newissuebutton->value = true; + $newissuebutton->label = get_string('newissue', 'local_qtracker'); + $newissueurl = new \moodle_url('/local/qtracker/new_issue.php'); + $newissueurl->param('courseid', $this->courseid); + $newissuebutton->action = $newissueurl; + $data->newissuebutton = $newissuebutton; + + + $data->action = $PAGE->url; + + $question = \question_bank::load_question($this->questionissue->get_questionid()); + question_require_capability_on($question, 'use'); + + $questiondata = new stdClass(); + $questiondata->questionid = $question->id; + $questiondata->questionname = $question->name; + $questiondata->preview_url = question_preview_url($question->id, null, null, null, null, $context); + + $editurl = new \moodle_url('/question/question.php'); + $editurl->param('id', $question->id); + $editurl->param('courseid', $this->courseid); + $returnurl = $PAGE->url->out_as_local_url(false); + $editurl->param('returnurl', $returnurl); + $questiondata->edit_url = $editurl; + + $form = new question_details_form($question, $PAGE->url); + $questiondata->questiontext = $form->render(); + $data->question = $questiondata; + $data->courseid = $this->courseid; + + // Setup text editor. + $editor = editors_get_preferred_editor(FORMAT_HTML); + $options = array(); + $editor->use_editor('commenteditor', $options); + + return $data; + } +} diff --git a/classes/output/question_issues_page.php b/classes/output/question_issues_page.php new file mode 100644 index 0000000..cab1907 --- /dev/null +++ b/classes/output/question_issues_page.php @@ -0,0 +1,84 @@ +. + +/** + * Renderable for issues page + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\output; + +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use dml_exception; +use moodle_exception; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Class containing data for question issues page. + * + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_issues_page implements renderable, templatable { + + /** The default number of results to be shown per page. */ + const DEFAULT_PAGE_SIZE = 20; + + /** @var array|question_issues_table|\local_qtracker\question_issues_table */ + protected $questionissuestable = []; + + /** + * Construct this renderable. + * + * @param \local_qtracker\question_issues_table $questionissuestable + */ + public function __construct(question_issues_table $questionissuestable, $courseid) { + $this->questionissuestable = $questionissuestable; + $this->courseid = $courseid; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + + ob_start(); + $this->questionissuestable->out(self::DEFAULT_PAGE_SIZE, true); + $questionissues = ob_get_contents(); + ob_end_clean(); + $data->questionissues = $questionissues; + $data->courseid = $this->courseid; + + + return $data; + } +} diff --git a/classes/output/question_issues_table.php b/classes/output/question_issues_table.php new file mode 100644 index 0000000..c4dbd8b --- /dev/null +++ b/classes/output/question_issues_table.php @@ -0,0 +1,216 @@ +. + +/** + * Table of question issues. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/tablelib.php'); + +use context_system; +use moodle_url; +use table_sql; + +/** + * Question issues table. + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_issues_table extends table_sql { + + /** + * Sets up the table. + * + * @param string $uniqueid Unique id of table. + * @param moodle_url $url The base URL. + */ + public function __construct($uniqueid, $url, $context, $manuallySorted) { + global $CFG; + parent::__construct($uniqueid); + + $this->context = $context; + // Define columns in the table. + $this->define_table_columns(); + // Set the baseurl. + $this->define_baseurl($url); + // Define configs. + $this->define_table_configs(); + // Define SQL. + $this->setup_sql_queries(); + } + + /** + * Generate the display of the id column. + * @param object $data the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_id($data) { + global $COURSE; + + if ($data->id) { + $issueid = $data->id; + $url = new \moodle_url('/local/qtracker/issue.php'); + $url->param('courseid', $COURSE->id); + $url->param('issueid', $issueid); + $id = \html_writer::link($url, $data->id); + return $id; + } else { + return '-'; + } + } + + /** + * Generate the display of the question name column. + * @param object $data the table row being output. + * @return string HTML content to go inside the td. + */ + protected function col_questionid($data) { + if ($data->questionid) { + return $data->questionid; + } else { + return '-'; + } + } + + /** + * Generate the display of the title. + * @param object $data the table row being output. + * @return string HTML content to go inside the td. + */ + protected function col_title($data) { + global $COURSE; + if ($data->title) { + $issueid = $data->id; + $url = new \moodle_url('/local/qtracker/issue.php'); + $url->param('courseid', $COURSE->id); + $url->param('issueid', $issueid); + $title = \html_writer::link($url, $data->title); + return $title; // need to change it to correct link. + // return ''.$data->title.''; + } else { + return '-'; + } + } + + /** + * Generate the display of the description. + * @param object $data the table row being output. + * @return string HTML content to go inside the td. + */ + protected function col_description($data) { + if ($data->description) { + return $data->description; + } else { + return '-'; + } + } + + /** + * The timecreated column. + * @param stdClass $data The row data. + * @return string + */ + public function col_timecreated($data) { + return userdate($data->timecreated); + } + + /** + * TODO: touch up this + * Setup the headers for the table. + */ + protected function define_table_columns() { + + // Define headers and columns. + // TODO: define strings in lang file. + $cols = array( + 'id' => get_string('id', 'local_qtracker'), + 'questionid' => get_string('questionid', 'local_qtracker'), + 'title' => get_string('title', 'local_qtracker'), + 'description' => get_string('description', 'local_qtracker'), + 'timecreated' => get_string('timecreated', 'local_qtracker') + ); + + $this->define_columns(array_keys($cols)); + $this->define_headers(array_values($cols)); + } + + /** + * Define table configs. + */ + protected function define_table_configs() { + $this->collapsible(false); + $this->sortable(true); + $this->pageable(true); + } + + /** + * Builds the SQL query. + * + * @return array containing sql to use and an array of params. + */ + public function setup_sql_queries() { + global $DB; + + $contextids = explode('/', trim($this->context->path, '/')); + // Get all child contexts. + $children = $this->context->get_child_contexts(); + foreach ($children as $c) { + $contextids[] = $c->id; + } + + list($insql, $inarams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); + + + // TODO: Write SQL to retrieve all rows... + $fields = 'DISTINCT '; + $fields .= 'qi.*'; + $from = '{local_qtracker_issue} qi'; + $from .= "\nJOIN {context} ctx ON qi.contextid = ctx.id"; + $where = "\nctx.id $insql"; + $params = $inarams; + + // The WHERE clause is vital here, because some parts of tablelib.php will expect to + // add bits like ' AND x = 1' on the end, and that needs to leave to valid SQL. + $this->set_count_sql("SELECT COUNT(1) FROM (SELECT $fields FROM $from WHERE $where) temp WHERE 1 = 1", $params); + + list($fields, $from, $where, $params) = $this->update_sql_after_count($fields, $from, $where, $params); + $this->set_sql($fields, $from, $where, $params); + } + + /** + * A chance for subclasses to modify the SQL after the count query has been generated, + * and before the full query is constructed. + * @param string $fields SELECT list. + * @param string $from JOINs part of the SQL. + * @param string $where WHERE clauses. + * @param array $params Query params. + * @return array with 4 elements ($fields, $from, $where, $params) as from base_sql. + */ + protected function update_sql_after_count($fields, $from, $where, $params) { + return [$fields, $from, $where, $params]; + } +} diff --git a/classes/output/questions_page.php b/classes/output/questions_page.php new file mode 100644 index 0000000..5677732 --- /dev/null +++ b/classes/output/questions_page.php @@ -0,0 +1,109 @@ +. + +/** + * Renderable for questions page + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\output; + +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use dml_exception; +use moodle_exception; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Class containing data for question page. + * + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class questions_page implements renderable, templatable { + + /** The default number of results to be shown per page. */ + const DEFAULT_PAGE_SIZE = 20; + + /** @var array|questions_table|\local_qtracker\questions_table */ + protected $questionstable = []; + + /** + * Construct this renderable. + * + * @param \local_qtracker\questions_table $questionstable + * @param int $courseid the id of the course + */ + public function __construct(questions_table $questionstable, $courseid) { + $this->questionstable = $questionstable; + $this->courseid = $courseid; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + */ + public function export_for_template(renderer_base $output) { + global $PAGE; + $data = new stdClass(); + + + $newissuebutton = new stdClass(); + $newissuebutton->primary = true; + $newissuebutton->name = "newissue"; + $newissuebutton->value = true; + $newissuebutton->label = get_string('newissue', 'local_qtracker'); + $newissueurl = new \moodle_url('/local/qtracker/new_issue.php'); + $newissueurl->param('courseid', $this->courseid); + $newissuebutton->action = $newissueurl; + $data->newissuebutton = $newissuebutton; + + $allissuesbutton = new stdClass(); + $allissuesbutton->primary = true; + $allissuesbutton->name = "newissue"; + $allissuesbutton->value = true; + $allissuesbutton->classes = "pr-2"; + $allissuesbutton->label = get_string('allissues', 'local_qtracker'); + $allissuesurl = new \moodle_url('/local/qtracker/issues.php'); + $allissuesurl->param('courseid', $this->courseid); + $allissuesbutton->action = $allissuesurl; + $data->allissuesbutton = $allissuesbutton; + + + + ob_start(); + $this->questionstable->out(self::DEFAULT_PAGE_SIZE, true); + $questions = ob_get_contents(); + ob_end_clean(); + $data->questions = $questions; + $data->courseid = $this->courseid; + + return $data; + } +} diff --git a/classes/output/questions_table.php b/classes/output/questions_table.php new file mode 100644 index 0000000..9a7e32c --- /dev/null +++ b/classes/output/questions_table.php @@ -0,0 +1,252 @@ +. + +/** + * Table of questions with registered issues. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/tablelib.php'); + +use context_system; +use moodle_url; +use table_sql; + +/** + * Questions table. + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class questions_table extends table_sql { + + /** + * Sets up the table. + * + * @param string $uniqueid Unique id of table. + * @param moodle_url $url The base URL. + * @param \context_course $context + * @param boolean $manuallySorted set true if user has chosen a non-default sorting configuration + */ + public function __construct($uniqueid, $url, $context, $manuallySorted = false) { + global $CFG; + parent::__construct($uniqueid); + // TODO: determine which context to use... + $this->context = $context; + + // Define columns in the table. + $this->define_table_columns(); + // Set the baseurl. + $this->define_baseurl($url); + // Define configs. + $this->define_table_configs($manuallySorted); + // Define SQL. + $this->setup_sql_queries(); + } + + /** + * Generate the display of the question name column. + * @param object $data the table row being output. + * @return string HTML content to go inside the td. + */ + protected function col_id($data) { + if ($data->id) { + return $data->id; + } else { + return '-'; + } + } + + /** + * Generate the display of the title. + * @param object $data the table row being output. + * @return string HTML content to go inside the td. + */ + protected function col_name($data) { + if ($data->name) { + $id = $data->id; + $name = \html_writer::link("#", $data->name, array('onclick' => "showIssuesInPane($id);return false;")); + return $name; // need to change it to correct link. + // return ''.$data->title.''; + } else { + return '-'; + } + } + + /** + * Generate the display of the new, open and close column + * @param string $cols extra_colums (new, open and close) + * @param object $data the table row being output + * @return |null string html content to go inside the td. + */ + public function other_cols($cols, $data) { + switch ($cols) { + case 'new': + case 'open': + case 'closed': + $nrofstate = $data->{$cols}; + if ($nrofstate < 1) { + return $nrofstate; + } + $id = $data->id; + $closed = \html_writer::link("#", $nrofstate, array('onclick' => "showIssuesInPane($id, '$cols');return false;")); + return $closed; + default: + return null; + } + } + + /** + * TODO: touch up this + * Setup the headers for the table. + */ + protected function define_table_columns() { + + // Define headers and columns. + // TODO: define strings in lang file. + $cols = array( + 'id' => get_string('questionid', 'local_qtracker'), + 'name' => get_string('name', 'local_qtracker'), + 'new' => get_string('new', 'local_qtracker'), + 'open' => get_string('open', 'local_qtracker'), + 'closed' => get_string('closed', 'local_qtracker') + ); + + $this->define_columns(array_keys($cols)); + $this->define_headers(array_values($cols)); + } + + + /** + * Define table configs. + * @param boolean $manuallySorted if false the default sorting will be used + */ + protected function define_table_configs($manuallySorted) { + $this->collapsible(false); + $this->sortable(true); + $this->pageable(true); + if (!$manuallySorted) { + $sortdata = [ + [ + 'sortby' => 'new', + 'sortorder' => SORT_DESC, + ], [ + 'sortby' => 'open', + 'sortorder' => SORT_DESC, + ], [ + 'sortby' => 'closed', + 'sortorder' => SORT_DESC, + ], [ + 'sortby' => 'id', + 'sortorder' => SORT_DESC, + ] + ]; + $this->set_sortdata($sortdata); + } + } + + /** + * Builds the SQL query. + * + * @return array containing sql to use and an array of params. + */ + public function setup_sql_queries() { + global $DB; + + $contextids = explode('/', trim($this->context->path, '/')); + // Get all child contexts. + $children = $this->context->get_child_contexts(); + foreach ($children as $c) { + $contextids[] = $c->id; + } + + list($insql, $inarams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); + + $fields = 'q.id, + q.name,'; + $fields .= "COUNT(case qi.state when 'new' then 1 else null end) AS new, + COUNT(case qi.state when 'open' then 1 else null end) AS open, + COUNT(case qi.state when 'closed' then 1 else null end) AS closed"; + $from = '{local_qtracker_issue} qi'; + $from .= "\nJOIN {question} q ON q.id = qi.questionid"; + $from .= "\nJOIN {context} ctx ON qi.contextid = ctx.id"; + $where = "\nctx.id $insql"; + $where .= "\nGROUP BY q.id"; + $params = $inarams; + + // The WHERE clause is vital here, because some parts of tablelib.php will expect to + // add bits like ' AND x = 1' on the end, and that needs to leave to valid SQL. + $this->set_count_sql("SELECT COUNT(1) FROM (SELECT $fields FROM $from WHERE $where) temp WHERE 1 = 1", $params); + + list($fields, $from, $where, $params) = $this->update_sql_after_count($fields, $from, $where, $params); + $this->set_sql($fields, $from, $where, $params); + } + + /** + * A chance for subclasses to modify the SQL after the count query has been generated, + * and before the full query is constructed. + * @param string $fields SELECT list. + * @param string $from JOINs part of the SQL. + * @param string $where WHERE clauses. + * @param array $params Query params. + * @return array with 4 elements ($fields, $from, $where, $params) as from base_sql. + */ + protected function update_sql_after_count($fields, $from, $where, $params) { + return [$fields, $from, $where, $params]; + } + + + /** + * Not in use + */ + public function wrap_html_start() { + if ($this->is_downloading()) { + return; + } + + // echo '
'; + // echo '
'; + // echo '
'; + // echo '
'; + // echo '
'; + // echo '
'; + + } + + /** + * Not in use + */ + public function wrap_html_finish() { + global $PAGE; + if ($this->is_downloading()) { + return; + } + + // echo '
'; + // echo '
'; + // echo '
'; + // echo '
'; + } +} diff --git a/classes/output/renderer.php b/classes/output/renderer.php new file mode 100644 index 0000000..50ae6ac --- /dev/null +++ b/classes/output/renderer.php @@ -0,0 +1,103 @@ +. + +/** + * Renderer + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\output; + +defined('MOODLE_INTERNAL') || die(); + +use local_qtracker\output\email\issue_comment_email; +use plugin_renderer_base; +use templatable; + +/** + * Question Tracker renderer. + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Render the review page for the deletion of expired contexts. + * + * @param question_issues_page $page + * @return string html for the page + * @throws moodle_exception + */ + public function render_new_question_issue_page(new_question_issue_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('local_qtracker/new_question_issue_page', $data); + } + + /** + * Render the review page for the deletion of expired contexts. + * + * @param question_issues_page $page + * @return string html for the page + * @throws moodle_exception + */ + public function render_question_issues_page(question_issues_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('local_qtracker/question_issues_page', $data); + } + + /** + * Render the review page for the deletion of expired contexts. + * + * @param questions_page $page + * @return string html for the page + * @throws moodle_exception + */ + public function render_questions_page(questions_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('local_qtracker/questions_page', $data); + } + + /** + * Renders a block. + * + * @param templatable $block Renderable of block content. + * @return string + * @throws \moodle_exception + */ + public function render_block(templatable $block) { + $content = ''; + $data = $block->export_for_template($this); + $content .= $this->render_from_template('local_qtracker/issue_registration_block', $data); + return $content; + } + + /** + * Renders a issue comment email. + * + * @param issue_comment_email $comment The issue comment to display. + * @return string + */ + public function render_issue_comment_email(issue_comment_email $comment) { + $data = $comment->export_for_template($this, $this->target === RENDERER_TARGET_TEXTEMAIL); + return $this->render_from_template('local_qtracker/' . $this->get_template_name(), $data); + } +} diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php new file mode 100644 index 0000000..adcee5b --- /dev/null +++ b/classes/privacy/provider.php @@ -0,0 +1,274 @@ +. + +/** + * Privacy Subsystem implementation for local_qtracker. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker\privacy; + +use coding_exception; +use context; +use context_module; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\helper; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; +use dml_exception; +use local_qtracker\issue; +use moodle_exception; +use question_display_options; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem implementation for local_qtracker. + * + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // This plugin has data. + \core_privacy\local\metadata\provider, + + // This plugin currently implements the original plugin_provider interface. + \core_privacy\local\request\plugin\provider { + + /** + * Returns meta data about this system. + * @param collection $items The initialised collection to add metadata to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $items): collection { + // The table 'local_qtracker_issue' stores a record for each qtracker issue. + // It contains a userid which links to the user that created the issue and contains information about that issue. + $items->add_database_table('local_qtracker_issue', [ + 'userid' => 'privacy:metadata:local_qtracker_issue:userid', + 'title' => 'privacy:metadata:local_qtracker_issue:title', + 'description' => 'privacy:metadata:local_qtracker_issue:description', + 'timecreated' => 'privacy:metadata:local_qtracker_issue:timecreated' + ], 'privacy:metadata:local_qtracker_issue'); + + // The table 'local_qtracker_comment' stores a record of each issue comment. + // It contains a userid which links to the user that created the comment and contains information about that comment. + $items->add_database_table('local_qtracker_comment', [ + 'userid' => 'privacy:metadata:local_qtracker_comment:userid', + 'description' => 'privacy:metadata:local_qtracker_comment:description', + 'timecreated' => 'privacy:metadata:local_qtracker_comment:timecreated' + ], 'privacy:metadata:local_qtracker_comment'); + + return $items; + } + + /** + * Get the list of contexts where the specified user has attempted a capquiz. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid): contextlist { + + // TODO: select all from table local_qtracker_issue and local_qtracker_comment left join? on issueid (comments table) to get all contextids stored in the local_qtracker_issue table. + $sql = "SELECT qi.contextid + FROM {local_qtracker_issue} qi + LEFT JOIN {local_qtracker_comment} qc + ON qi.id = qc.issueid + WHERE qi.userid = :userid1 + OR qc.userid = :userid2"; + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, [ + 'userid1' => $userid, + 'userid2' => $userid + ]); + return $contextlist; + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + if (empty($contextlist)) { + return; + } + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $sql = "SELECT * FROM {local_qtracker_issue} WHERE contextid $contextsql"; + $issues = $DB->get_records_sql($sql, $contextparams); + + $context = null; + foreach ($issues as $issue) { + $context = context::instance_by_id($issue->contextid); + // Store the quiz attempt data. + $data = new stdClass(); + $data->title = $issue->title; + $data->description = $issue->description; + $data->timecreated = transform::datetime($issue->timecreated); + + $subcontext = [get_string('issues', 'local_qtracker'), + get_string('issue', 'local_qtracker') . ' ' . $issue->id]; + // The capquiz attempt data is organised in: {Course name}/{Qtracker}/{Issues}/{_X}/data.json + // where X is the attempt number. + writer::with_context($context)->export_data($subcontext, $data); + //writer::with_context($context)->export_area_files($subcontext, 'local_qtracker', 'description', $issue->id); + } + + $user = $contextlist->get_user(); + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $sql = "SELECT qc.id AS id, + qi.contextid AS contextid, + qi.id AS issueid, + qc.description AS description, + qc.timecreated AS timecreated + FROM {local_qtracker_issue} qi + INNER JOIN {local_qtracker_comment} qc + ON qi.id = qc.issueid + WHERE qc.userid = :userid + AND qi.contextid {$contextsql}"; + $params = [ + 'userid' => $user->id + ]; + $params += $contextparams; + $comments = $DB->get_records_sql($sql, $params); + + $context = null; + foreach ($comments as $comment) { + $context = context::instance_by_id($comment->contextid); + // Store the quiz attempt data. + $data = new stdClass(); + $data->description = $comment->description; + $data->timecreated = transform::datetime($comment->timecreated); + + $subcontext = [get_string('issues', 'local_qtracker'), + get_string('issue','local_qtracker') . ' ' . $comment->issueid, + get_string('comments', 'local_qtracker'), + get_string('comment', 'local_qtracker') . ' ' . $comment->id]; + // The issue comment data is organised in: {Course name}/{Qtracker}/{Issues}/{_X}/Comments({_Y}/data.json + // where X is the issue id and Y is the comment id. + writer::with_context($context)->export_data($subcontext, $data); + //writer::with_context($context)->export_area_files($subcontext, 'local_qtracker', 'description', $comment->id); + } + } + + /** + * Delete all data for all users in the specified context. + * + * @param context $context The specific context to delete data for. + */ + public static function delete_data_for_all_users_in_context(context $context) { + global $DB; + $sql = "SELECT qc.id AS id + FROM {local_qtracker_issue} qi + INNER JOIN {local_qtracker_comment} qc + ON qc.issueid = qi.id + WHERE qi.contextid = :contextid"; + $params = [ + 'contextid' => $context->id + ]; + $comments = $DB->get_records_sql($sql, $params); + + foreach ($comments as $comment) { + $DB->delete_records('local_qtracker_comment', ['id' => $comment->id]); + } + + $sql = "SELECT id + FROM {local_qtracker_issue} + WHERE contextid = :contextid"; + $params = [ + 'contextid' => $context->id + ]; + $issues = $DB->get_records_sql($sql, $params); + + $issueids = []; + foreach ($issues as $issue) { + array_push($issueids, $issue->id); + $DB->delete_records('local_qtracker_issue', ['id' => $issue->id]); + } + + list($issueidsql, $issueidparams) = $DB->get_in_or_equal($issueids, SQL_PARAMS_NAMED); + $refssql = "SELECT id FROM {local_qtracker_reference} WHERE sourceid $issueidsql OR targetid $issueidsql"; + $refs = $DB->get_records_sql($refssql, $issueidparams); + foreach ($refs as $ref) { + $DB->delete_records('local_qtracker_reference', ['id' => $ref->id]); + } + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + if (empty($contextlist->count())) { + return; + } + $user = $contextlist->get_user(); + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $sql = "SELECT qc.id AS id + FROM {local_qtracker_issue} qi + INNER JOIN {local_qtracker_comment} qc + ON qc.issueid = qi.id + WHERE qc.userid = :userid + AND qi.contextid {$contextsql}"; + $params = [ + 'userid' => $user->id + ]; + $params += $contextparams; + $comments = $DB->get_records_sql($sql, $params); + + foreach ($comments as $comment) { + $DB->delete_records('local_qtracker_comment', ['id' => $comment->id]); + } + + $sql = "SELECT id + FROM {local_qtracker_issue} + WHERE userid = :userid + AND contextid {$contextsql}"; + $params = [ + 'userid' => $user->id + ]; + $params += $contextparams; + $issues = $DB->get_records_sql($sql, $params); + + $issueids = []; + foreach ($issues as $issue) { + array_push($issueids, $issue->id); + $DB->delete_records('local_qtracker_issue', ['id' => $issue->id]); + } + + list($issueidsql, $issueidparams) = $DB->get_in_or_equal($issueids, SQL_PARAMS_NAMED); + $refssql = "SELECT id FROM {local_qtracker_reference} WHERE sourceid $issueidsql OR targetid $issueidsql"; + $refs = $DB->get_records_sql($refssql, $issueidparams); + foreach ($refs as $ref) { + $DB->delete_records('local_qtracker_reference', ['id' => $ref->id]); + } + } +} diff --git a/classes/referable.php b/classes/referable.php new file mode 100644 index 0000000..485d375 --- /dev/null +++ b/classes/referable.php @@ -0,0 +1,106 @@ +. + +/** + * Referable class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +/** + * Referable class. + * + * @package local_qtracker + * @copyright 2021 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class referable { + /** + * @var array + */ + protected $incomingrefs = array(); + + /** + * @var array + */ + protected $outgoingrefs = array(); + + /** + * Get this referable's id. + */ + abstract public function get_id(); + + /** + * Create a new outgoing reference. + * The source is $this referable. + * @param referable $target The target referable to create an connection to + * @param string $type The reference type to create + */ + public function make_outgoing_reference(referable $target, string $type) { + $reference = reference::create($this->get_id(), $target->get_id(), $type); + array_push($this->outgoingrefs, $reference); + } + + /** + * Create a new ingoing reference. + * The target is $this referable. + * @param referable $source The source referable to make an reference from + * @param string $type The reference type to create + */ + public function make_incoming_reference(referable $source, string $type) { + $reference = reference::create($source->get_id(), $this->get_id(), $type); + array_push($this->outgoingrefs, $reference); + } + + /** + * Get all outcoing references from this referable. + * + * @return array + */ + public function get_outgoing_references() { + global $DB; + $this->otugoingrefs = array(); + $otugoingrefs = $DB->get_records('local_qtracker_reference', ['sourceid' => $this->get_id()]); + foreach ($otugoingrefs as $otugoingref) { + array_push($this->otugoingrefs, new reference($otugoingref)); + } + return $this->otugoingrefs; + } + + /** + * Get all incomming references to this issue. + * + * @return array + */ + public function get_incoming_references() { + global $DB; + $this->incomingrefs = array(); + $incomingrefs = $DB->get_records('local_qtracker_reference', ['targetid' => $this->get_id()]); + foreach ($incomingrefs as $incomingref) { + array_push($this->incomingrefs, new reference($incomingref)); + } + return $this->incomingrefs; + } +} diff --git a/classes/reference.php b/classes/reference.php new file mode 100644 index 0000000..a6a9f6a --- /dev/null +++ b/classes/reference.php @@ -0,0 +1,177 @@ +. + +/** + * Issue reference class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +/** + * QTracker reference class. + * + * @package local_qtracker + * @copyright 2021 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reference { + + /** + * @var \stdClass + */ + protected $reference = null; + + /** + * Constructor. + * + * @param int|\stdClass $reference + * @return void + */ + public function __construct($reference) { + global $DB; + if (is_scalar($reference)) { + $reference = $DB->get_record('local_qtracker_reference', array('id' => $reference), '*', MUST_EXIST); + if (!$reference) { + throw new \moodle_exception('errorunexistingmodel', 'analytics', '', $reference); + } + } + $this->reference = $reference; + } + + /** + * Returns the reference id. + * + * @return int + */ + public function get_id() { + return $this->reference->id; + } + + /** + * Returns the related source id. + * + * @return int + */ + public function get_source_id() { + return $this->reference->sourceid; + } + + /** + * Returns the related target id. + * + * @return int + */ + public function get_target_id() { + return $this->reference->targetid; + } + + /** + * Returns the reference type. + * + * @return string + */ + public function get_reftype() { + return $this->reference->reftype; + } + + /** + * Returns a plain \stdClass with the reference data. + * + * @return \stdClass + */ + public function get_reference_obj() { + return $this->reference; + } + + /** + * Loads and returns reference with id $reference + * + * @param int $reference + * + * @return reference + */ + public static function load(int $reference) { + global $DB; + $referenceobj = $DB->get_record('local_qtracker_reference', ['id' => $reference]); + if ($referenceobj === false) { + return null; + } + return new reference($referenceobj); + } + + /** + * Creates a new reference. + * + * @param int $sourceid reference id + * @param int $targetid reference id + * @param string $reftype + * + * @return reference + */ + public static function create(int $sourceid, int $targetid, string $reftype) { + global $USER, $DB; + + $referenceobj = new \stdClass(); + $referenceobj->sourceid = $sourceid; + $referenceobj->targetid = $targetid; + if (is_reference_type($reftype)) { + $referenceobj->reftype = $reftype; + } else { + throw new coding_exception('Not a valid reference type ' . $reftype); + } + $id = $DB->insert_record('local_qtracker_reference', $referenceobj); + $referenceobj->id = $id; + + $reference = new reference($referenceobj); + return $reference; + } + + /** + * Delete this reference. + * + * @return void + */ + public function delete() { + global $DB; + return $DB->delete_records('local_qtracker_reference', array('id' => $this->get_id())); + } + + /** + * Sets the reference type of this reference. + * + * @param string $type + * @throws \coding_exception + * @return void + */ + public function set_reftype($type) { + global $DB; + if (is_reference_type($type)) { + $this->reference->reftype = $type; + $DB->update_record('local_qtracker_reference', $this->reference); + } else { + throw new coding_exception('Not a valid reference type ' . $type); + } + } +} diff --git a/classes/reference_manager.php b/classes/reference_manager.php new file mode 100644 index 0000000..3859b5b --- /dev/null +++ b/classes/reference_manager.php @@ -0,0 +1,116 @@ +. + +/** + * Issue class + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use local_qtracker\referable_interface; + +/** + * Question issue class. + * + * @package local_qtracker + * @copyright 2020 André Storhaug + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reference_manager { + + /** + * @var referable + */ + protected $referable = null; + + /** + * @var \array + */ + protected $incomingrefs = array(); + + /** + * @var \array + */ + protected $outgoingrefs = array(); + + /** + * Constructs with item details. + * + * @param int $userid Userid for modinfo (if used) + * @param \cm_info $cm Course-module object + */ + public function __construct(referable $referable) { + $this->referable = $referable; + } + + /** + * Get all incomming references to this issue. + * + * @return \stdClass + */ + public function get_incoming_references() { + global $DB; + if (empty($this->incomingrefs)) { + $this->incomingrefs = array(); + $incomingrefs = $DB->get_records('local_qtracker_reference', ['targetid' => $this->referable->get_id()]); + foreach ($incomingrefs as $incomingref) { + array_push($this->incomingrefs, new reference($incomingref)); + } + } + return $this->incomingrefs; + } + + /** + * Get all outcoing references from this issue. + * + * @return \stdClass + */ + public function get_outgoing_references() { + global $DB; + if (empty($this->otugoingrefs)) { + $this->otugoingrefs = array(); + $otugoingrefs = $DB->get_records('local_qtracker_reference', ['sourceid' => $this->get_id()]); + foreach ($otugoingrefs as $otugoingref) { + array_push($this->otugoingrefs, new reference($otugoingref)); + } + } + return $this->otugoingrefs; + } + + /** + * Filter references by type + * + * @return array + */ + public static function filter_references_by_type(array $references, string $type) { + $filteredrefs = array(); + foreach ($references as $reference) { + if ($reference->get_reftype() == $type) { + array_push($filteredrefs, $reference); + } + } + return $filteredrefs; + } +} diff --git a/db/access.php b/db/access.php new file mode 100644 index 0000000..d09dd6f --- /dev/null +++ b/db/access.php @@ -0,0 +1,68 @@ +. + +/** + * This file contains the capabilities defined for the qtracter module + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$capabilities = array( + + 'local/qtracker:addissue' => array( + 'riskbitmask' => RISK_SPAM | RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + ), + 'local/qtracker:editmine' => array( + 'riskbitmask' => RISK_SPAM | RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + ), + 'local/qtracker:editall' => array( + 'riskbitmask' => RISK_SPAM | RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + ), + 'local/qtracker:viewmine' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + ), + 'local/qtracker:viewall' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + ), +); diff --git a/db/events.php b/db/events.php new file mode 100644 index 0000000..13ff8cd --- /dev/null +++ b/db/events.php @@ -0,0 +1,33 @@ +. + +/** + * This file defines observers needed by the plugin. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$observers = [ + [ + 'eventname' => '\core\event\question_deleted', + 'callback' => '\local_qtracker\event\question_deleted_observer::question_deleted', + ], +]; diff --git a/db/install.xml b/db/install.xml new file mode 100755 index 0000000..d2b323a --- /dev/null +++ b/db/install.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+
+
diff --git a/db/messages.php b/db/messages.php new file mode 100644 index 0000000..c456578 --- /dev/null +++ b/db/messages.php @@ -0,0 +1,38 @@ + +. + +/** + * This file defines message providers provided by the plugin. + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +$messageproviders = array ( + 'issueresponse' => array ( + 'defaults' => array ( + 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF + MESSAGE_DEFAULT_LOGGEDIN, + 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF, + ) + ) +); diff --git a/db/services.php b/db/services.php new file mode 100644 index 0000000..4b9a111 --- /dev/null +++ b/db/services.php @@ -0,0 +1,163 @@ +. + +/** + * This file contains the services for the qtracter module + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +// We defined the web service functions to install. +$functions = array( + 'local_qtracker_new_issue' => array( + 'classname' => 'local_qtracker\external\new_issue', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Register a new question issue.', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => array(), // Capabilities required by the function. + 'loginrequired' => true, + ), + 'local_qtracker_edit_issue' => array( + 'classname' => 'local_qtracker\external\edit_issue', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Edit an existing question issue.', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => array(), // Capabilities required by the function. + 'loginrequired' => true, + ), + 'local_qtracker_delete_issue' => array( + 'classname' => 'local_qtracker\external\delete_issue', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Delete an existing question issue.', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => array(), // Capabilities required by the function. + 'loginrequired' => true, + ), + 'local_qtracker_get_issue' => array( + 'classname' => 'local_qtracker\external\get_issue', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Get an existing question issue.', + 'type' => 'read', + 'ajax' => true, + 'capabilities' => array(), // Capabilities required by the function. + 'loginrequired' => true, + ), + 'local_qtracker_get_question' => array( + 'classname' => 'local_qtracker\external\get_question', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Get question by id.', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => true, + ), + 'local_qtracker_get_question_preview_url' => array( + 'classname' => 'local_qtracker\external\get_question_preview_url', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Get question preview url.', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => true, + ), + 'local_qtracker_get_question_edit_url' => array( + 'classname' => 'local_qtracker\external\get_question_edit_url', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Get question edit url.', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => true, + ), + 'local_qtracker_get_issues' => array( + 'classname' => 'local_qtracker\external\get_issues', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Get issues.', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => true, + ), + 'local_qtracker_get_issue_parents' => array( + 'classname' => 'local_qtracker\external\get_issue_parents', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Get issue parents.', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => true, + ), + 'local_qtracker_get_issue_children' => array( + 'classname' => 'local_qtracker\external\get_issue_children', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Get issue children.', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => true, + ), + 'local_qtracker_set_issue_relation' => array( + 'classname' => 'local_qtracker\external\set_issue_relation', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Set issue relation.', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => array(), // Capabilities required by the function. + 'loginrequired' => true, + ), + 'local_qtracker_delete_issue_relation' => array( + 'classname' => 'local_qtracker\external\delete_issue_relation', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Delete issue relation.', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => array(), // Capabilities required by the function. + 'loginrequired' => true, + ) +); + +// We define the services to install as pre-build services. A pre-build service is not editable by administrator. +$services = array( + 'Question tracker service' => array( + 'functions' => array( + 'local_qtracker_new_issue', + 'local_qtracker_edit_issue', + 'local_qtracker_delete_issue', + 'local_qtracker_get_issue', + 'local_qtracker_get_issues', + 'local_qtracker_get_issue_parents', + 'local_qtracker_get_issue_children', + 'local_qtracker_set_issue_relation', + 'local_qtracker_delete_issue_relation', + ), + 'restrictedusers' => 0, + 'enabled' => 1, + ) +); diff --git a/db/upgrade.php b/db/upgrade.php new file mode 100755 index 0000000..cbe29b6 --- /dev/null +++ b/db/upgrade.php @@ -0,0 +1,41 @@ +. + +/** + * Upgrade + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Function to upgrade local_qtracker + * + * @param int $oldversion the version to be upgraded from + * @return bool result + */ +function xmldb_local_qtracker_upgrade($oldversion) { + global $DB; + $dbman = $DB->get_manager(); + + // TODO perform upgrades here... + + return true; +} diff --git a/issue.php b/issue.php new file mode 100644 index 0000000..8660506 --- /dev/null +++ b/issue.php @@ -0,0 +1,190 @@ +. + +/** + * This file is used to render the qtracker block + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker; + +use core\check\performance\debugging; +use core\message\message; +use local_qtracker\issue; +use local_qtracker\output\question_issue_page; +use local_qtracker\output\email\issue_comment_email; +use mod_capquiz\capquiz; + +require_once('../../config.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +global $DB, $OUTPUT, $PAGE, $USER; + +// Check for all required variables. +$courseid = required_param('courseid', PARAM_INT); +$issueid = required_param('issueid', PARAM_INT); + +$course = $DB->get_record('course', array('id' => $courseid)); +if (!$course) { + print_error('invalidcourseid'); +} + +require_login($course); + +$url = new \moodle_url('/local/qtracker/issue.php'); +$url->param('courseid', $courseid); +$url->param('issueid', $issueid); + +$returnurl = new \moodle_url('/local/qtracker/issues.php'); +$returnurl->param('courseid', $courseid); + +$PAGE->set_url($url); +$PAGE->set_pagelayout('incourse'); +$PAGE->set_heading(get_string('pluginname', 'local_qtracker')); + +// Return to issues list. +if (optional_param('return', false, PARAM_BOOL)) { + redirect($returnurl); +} + +$issuesnode = $PAGE->navbar->add( + get_string('pluginname', 'local_qtracker'), + null, + \navigation_node::TYPE_CONTAINER, + null, + 'qtracker' +); +$issuesnode->add( + get_string('issues', 'local_qtracker'), + new \moodle_url('/local/qtracker/view.php', array('courseid' => $courseid)) +); +$issuesnode->add(get_string('issue', 'local_qtracker')); + + + +// Load issue. +$issue = issue::load($issueid); +if (!$issue) { + $issuesurl = new \moodle_url('/local/qtracker/view.php', array('courseid' => $courseid)); + redirect($issuesurl); +} + +/** + * Send comment as message. + */ +function send_comment($course, issue $issue, issue_comment $comment) { + global $DB, $USER, $PAGE; + + $htmlemailrenderer = $PAGE->get_renderer('local_qtracker', 'email', 'htmlemail'); + $textemailrenderer = $PAGE->get_renderer('local_qtracker', 'email', 'textemail'); + + $recipient = $DB->get_record('user', array('id' => $issue->get_userid())); + $postsubject = get_string('issueupdatednotify', 'local_qtracker', $issue->get_title()); + + $data = new issue_comment_email($course, $comment, $USER, $recipient); + + $message = new \core\message\message(); + $message->component = 'local_qtracker'; + $message->name = 'issueresponse'; + $message->userfrom = $USER; + $message->userto = $recipient; + $message->subject = $postsubject; + $message->fullmessage = $textemailrenderer->render($data); + $message->fullmessageformat = FORMAT_HTML; + $message->fullmessagehtml = $htmlemailrenderer->render($data); + $message->smallmessage = ''; + $message->notification = 1; + $message->replyto = null; + + // TODO: make issue.php page handle correctly students viewing comments. + //$contexturl = new \moodle_url('/local/qtracker/issue.php', ['courseid' => $course->id, 'issueid' => $issue->get_id()]); + //$message->contexturl = $contexturl->out(); + //$message->contexturlname = $issue->get_title(); + + return message_send($message); +} + + +// Process issue actions. +$commentissue = optional_param('commentissue', false, PARAM_BOOL); +$commenttext = optional_param('commenteditor', false, PARAM_RAW); +$sendmessage = optional_param('sendmessage', false, PARAM_BOOL); + +if ($commentissue) { + $comment = $issue->create_comment($commenttext); + if ($sendmessage) { + if (send_comment($course, $issue, $comment)) { + $comment->mark_mailed(); + } + } + redirect($PAGE->url); +} + +$closeissue = optional_param('closeissue', false, PARAM_BOOL); +if ($closeissue) { + if ($commenttext != false) { + $comment = $issue->create_comment($commenttext); + if ($sendmessage) { + if (send_comment($course, $issue, $comment)) { + $comment->mark_mailed(); + } + } + } + $issue->close(); + redirect($PAGE->url); +} + +$reopenissue = optional_param('reopenissue', false, PARAM_BOOL); +if ($reopenissue) { + $issue->open(); + redirect($PAGE->url); +} + +$notifycommentid = optional_param('notifycommentid', null, PARAM_INT); +if (!is_null($notifycommentid)) { + $comment = issue_comment::load($notifycommentid); + if (send_comment($course, $issue, $comment)) { + $comment->mark_mailed(); + } + redirect($PAGE->url); +} + +$deletecommentid = optional_param('deletecommentid', null, PARAM_INT); +if (!is_null($deletecommentid)) { + $comment = issue_comment::load($deletecommentid); + $comment->delete(); + redirect($PAGE->url); +} + +// Capability checking. +issue_require_capability_on($issue->get_issue_obj(), 'view'); + +$renderer = $PAGE->get_renderer('local_qtracker'); +$questionissuepage = new question_issue_page($issue, $courseid); + +$data = $renderer->render($questionissuepage); + +echo $OUTPUT->header(); +echo $data; +echo $OUTPUT->footer(); + +if ($issue->get_state() == 'new') { + $issue->open(); +} diff --git a/issues.php b/issues.php new file mode 100644 index 0000000..c844fa0 --- /dev/null +++ b/issues.php @@ -0,0 +1,71 @@ +. + +/** + * Display qtracker question page + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker; + +require_once('../../config.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +global $DB, $OUTPUT, $PAGE; + +// Check for all required variables. +$courseid = required_param('courseid', PARAM_INT); +$manuallySorted = isset($_GET['tsort']); + +if (!$course = $DB->get_record('course', array('id' => $courseid))) { + print_error('invalidcourseid'); +} +$context = \context_course::instance($course->id); + +require_login($course); +require_capability('local/qtracker:viewall', $context); + +$url = new \moodle_url('/local/qtracker/issues.php', array('courseid' => $courseid)); + +$PAGE->set_url($url); +$PAGE->set_pagelayout('incourse'); +$PAGE->set_heading(get_string('pluginname', 'local_qtracker')); + + +$issuesnode = $PAGE->navbar->add( + get_string('pluginname', 'local_qtracker'), + null, \navigation_node::TYPE_CONTAINER, null, 'qtracker' +); +$issuesnode->add( + get_string('issues', 'local_qtracker'), + new \moodle_url('/local/qtracker/view.php', array('courseid' => $courseid)) +); +$issuesnode->add(get_string('allissues', 'local_qtracker')); + + +echo $OUTPUT->header(); + +// Get table renderer and display table. +$table = new \local_qtracker\output\question_issues_table(uniqid(), $url, $context, $manuallySorted); +$renderer = $PAGE->get_renderer('local_qtracker'); +$questionspage = new \local_qtracker\output\question_issues_page($table, $courseid); +echo $renderer->render($questionspage); + +echo $OUTPUT->footer(); diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php new file mode 100755 index 0000000..7c7858e --- /dev/null +++ b/lang/en/local_qtracker.php @@ -0,0 +1,138 @@ +. + +/** + * English keywords + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['pluginname'] = 'Question Tracker'; +$string['qtracker'] = 'Question Tracker'; +$string['qtracker:addinstance'] = 'Add a new Question Tracker block'; +$string['qtracker:myaddinstance'] = 'Add a new Question Tracker block to the My Moodle page'; + +$string['blockstring'] = 'Block string'; +$string['descconfig'] = 'Description of the config section'; +$string['descfoo'] = 'Config description'; +$string['headerconfig'] = 'Config section header'; +$string['labelfoo'] = 'Config label'; + +// Question issues table. +$string['id'] = 'ID'; +$string['questionid'] = 'Question ID'; +$string['title'] = 'Title'; +$string['description'] = 'Description'; +$string['timecreated'] = 'Created Time'; + +$string['question_problem_details'] = 'If you have feedback for this question, please type it below.'; +$string['cannotcreateissue'] = 'Cannot create a new question issue.'; +$string['cannoteditissue'] = 'Cannot edit question issue with ID {$a}.'; +$string['cannotdeleteissue'] = 'Cannot delete question issue with ID {$a}.'; +$string['cannotgetissue'] = 'Cannot get question issue with ID {$a}.'; +$string['cannotgetquestion'] = 'Cannot get question with ID {$a}.'; +$string['cannotgetissues'] = 'Cannot get question issues.'; +$string['question'] = 'Question'; +$string['question_help'] = 'Select the question you want to register a new issue for.'; + +$string['unknownquestionidnumber'] = 'Unknown question ID "{$a}"'; +$string['unknownqubaidnumber'] = 'Unknown question usage ID "{$a}"'; +$string['title'] = 'Title'; +$string['leavecomment'] = 'Leave a comment'; + +$string['issueupdatednotify'] = 'Issue "{$a}" is updated'; +$string['sendmessage'] = 'Notify issue reporter'; + +$string['issuecreated'] = 'Issue successfully created.'; +$string['issueupdated'] = 'Issue successfully updated.'; +$string['issuedeleted'] = 'Issue successfully deleted.'; + +$string['issuesuperseded'] = 'Issue is superseded by {$a}.'; + +$string['issue'] = 'Issue'; +$string['issues'] = 'Issues'; +$string['comments'] ='Comments'; + +$string['submitnewissue'] = 'Submit new issue'; +$string['validtitle'] = 'Please provide a valid title.'; +$string['validdescription'] = 'Please provide a valid description.'; + +$string['commentedon'] = 'commented on '; +$string['openedissueon'] = 'opened issue {$a} on '; +$string['errorsubsumingissue'] = 'An error occurred trying to subsume issue {$a}. The issue is probably superseded by another issue.'; +$string['name'] = 'Name'; + +$string['tags'] = 'Tags'; +$string['linkedissues'] = 'Linked issues'; + +$string['new'] = 'New'; +$string['open'] = 'Open'; +$string['closed'] = 'Closed'; +$string['newissue'] = 'New issue'; +$string['allissues'] = 'All issues'; + +$string['reopenissue'] = 'Reopen issue'; +$string['closeissue'] = 'Comment and close issue'; +$string['comment'] = 'Comment'; + +//TODO clean up the text +$string['commentandmail'] = 'Comment and forward mail to issue creator'; +$string['issuesubject'] = 'some subject'; +$string['commentanddm'] = 'Comment and dm issue to creator'; +$string['messageprovider:issueresponse'] = 'Response on reported question issues'; + + +$string['confirm'] = 'Confirm'; +$string['deletecomment'] = 'Delete comment'; +$string['confirmdeletecomment'] = 'Are you sure you want to delete this comment?'; +$string['sendcomment'] = 'Notify issue reporter'; +$string['confirmsendcomment'] = 'Are you sure you want to notify the issue reporter about this comment?'; + +$string['preview'] = 'Preview'; +$string['edit'] = 'Edit'; +$string['questionissues'] = 'Question issues'; + +$string['noitems'] = 'No items'; +$string['createnewissue'] = 'Create new issue'; +$string['errornonexistingquestion'] = 'Please provide an valid question ID.'; + +$string['subsumeissue'] = 'Subsume issue'; +$string['subsumeissueconfirm'] = 'Are you sure you want to subsume issue #{$a->child} under #{$a->parent}?
This will close issue #{$a->child}.'; +$string['subsumedissues'] = 'Subsumed issues'; +$string['subsumedescription'] = 'Subsume issues under this issue'; +$string['tagsdescription'] = 'Add tags for this issue'; + +$string['qtracker:addissue'] = 'Add new issue'; +$string['qtracker:editmine'] = 'Edit your own issues'; +$string['qtracker:editall'] = 'Edit all issues'; +$string['qtracker:viewmine'] = 'Edit your own issues'; +$string['qtracker:viewall'] = 'View all issues'; + +$string['privacy:metadata:local_qtracker_issue'] = 'Details about each question issue.'; +$string['privacy:metadata:local_qtracker_issue:userid'] = 'The user that created the issue.'; +$string['privacy:metadata:local_qtracker_issue:title'] = 'The title of the issue.'; +$string['privacy:metadata:local_qtracker_issue:description'] = 'The description of the issue.'; +$string['privacy:metadata:local_qtracker_issue:timecreated'] = 'The time the issue was created.'; + +$string['privacy:metadata:local_qtracker_comment'] = 'Details about each question issue comment.'; +$string['privacy:metadata:local_qtracker_comment:userid'] = 'The user that created the issue comment.'; +$string['privacy:metadata:local_qtracker_comment:description'] = 'The description of the issue comment.'; +$string['privacy:metadata:local_qtracker_comment:timecreated'] = 'The time the issue comment was created.'; diff --git a/lib.php b/lib.php new file mode 100644 index 0000000..3e41ee3 --- /dev/null +++ b/lib.php @@ -0,0 +1,153 @@ +. + +/** + * lib + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use local_qtracker\issue; + + +/** + * Define constants to store the referance type + */ +define('LOCAL_QTRACKER_REFERENCE_SUPERSEDED', 'superseded'); + + +/** + * This function extends the navigation with the report items + * + * @param navigation_node $navigation The navigation node to extend + * @param stdClass $course The course to object for the report + * @param stdClass $context The context of the course + */ +function local_qtracker_extend_navigation_course($navigation, $course, $context) { + global $CFG; + + if ($context->contextlevel == CONTEXT_COURSE) { + $params = array('courseid' => $context->instanceid); + } else if ($context->contextlevel == CONTEXT_MODULE) { + $params = array('cmid' => $context->instanceid); + } else { + return; + } + + $qtrackernode = $navigation->add( + get_string('pluginname', 'local_qtracker'), + null, + navigation_node::TYPE_CONTAINER, + null, + 'qtracker' + ); + + // TODO: Check if the user has ANY question issue context capabilities. + $qtrackernode->add(get_string('issues', 'local_qtracker'), new moodle_url( + $CFG->wwwroot . '/local/qtracker/view.php', + $params + ), navigation_node::TYPE_SETTING, null, 'issues'); +} + +/** + * Check capability on category + * + * @param mixed $issueorid object or id. If an object is passed, it should include ->contextid and ->userid. + * @param string $cap 'add', 'edit', 'view'. + * @return boolean this user has the capability $cap for this issue $issue? + */ +function issue_has_capability_on($issueorid, $cap) { + global $USER; + + if (is_numeric($issueorid)) { + $issue = issue::load((int)$issueorid)->get_issue_obj(); + } else if (is_object($issueorid)) { + if (isset($issueorid->contextid) && isset($issueorid->userid)) { + $issue = $issueorid; + } + + if (!isset($issue) && isset($issueorid->id) && $issueorid->id != 0) { + $issue = issue::load($issueorid->id)->get_issue_obj(); + } + } else { + throw new coding_exception('$issueorid parameter needs to be an integer or an object.'); + } + + $context = context::instance_by_id($issue->contextid); + + // These are existing issues capabilities. + // Each of these has a 'mine' and 'all' version that is appended to the capability name. + $capabilitieswithallandmine = ['edit' => 1, 'view' => 1]; + + if (!isset($capabilitieswithallandmine[$cap])) { + return has_capability('local/qtracker:' . $cap, $context); + } else { + return has_capability('local/qtracker:' . $cap . 'all', $context) || + ($issue->userid == $USER->id && has_capability('local/qtracker:' . $cap . 'mine', $context)); + } +} + +/** + * Require capability on issue. + * + * @param mixed $issue object or id. If an object is passed, it should include ->contextid and ->userid. + * @param string $cap 'add', 'edit', 'view'. + * + * @return boolean this user has the capability $cap for this issue $issue? + */ +function issue_require_capability_on($issue, $cap) { + if (!issue_has_capability_on($issue, $cap)) { + print_error('nopermissions', '', '', $cap); + } + return true; +} + +/** + * Check if reference type is valid. + * + * @param mixed $issue object or id. If an object is passed, it should include ->contextid and ->userid. + * @param string $cap 'add', 'edit', 'view'. + * + * @return boolean this user has the capability $cap for this issue $issue? + */ +function is_reference_type(string $type) { + $reftypes = array(LOCAL_QTRACKER_REFERENCE_SUPERSEDED); + + if (!in_array($type, $reftypes) ) { + return false; + } + return true; +} + +/** + * + * + * @param $feature + * @return bool true if a feature is supported + */ +function local_qtracker_supports($feature) { + switch($feature) { + case FEATURE_BACKUP_MOODLE2: + return true; + default: + return false; + } +} diff --git a/new_issue.php b/new_issue.php new file mode 100644 index 0000000..b1fa872 --- /dev/null +++ b/new_issue.php @@ -0,0 +1,92 @@ +. + +/** + * This file is used to render the qtracker block + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker; + +use local_qtracker\issue; +use local_qtracker\output\new_question_issue_page; +use local_qtracker\form\view\new_issue_form; + +require_once('../../config.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); +require_once($CFG->libdir . '/questionlib.php'); + +global $DB, $OUTPUT, $PAGE; + +// Check for all required variables. +$courseid = required_param('courseid', PARAM_INT); + +if (!$course = $DB->get_record('course', array('id' => $courseid))) { + print_error('invalidcourseid'); +} + +require_login($course); + +$url = new \moodle_url('/local/qtracker/new_issue.php'); +$url->param('courseid', $courseid); + +$returnurl = new \moodle_url('/local/qtracker/new_issues.php'); +$returnurl->param('courseid', $courseid); + +$PAGE->set_url($url); +$PAGE->set_pagelayout('incourse'); +$PAGE->set_heading(get_string('pluginname', 'local_qtracker')); + +// Return to issues list. +if (optional_param('return', false, PARAM_BOOL)) { + redirect($returnurl); +} + +$issuesnode = $PAGE->navbar->add( + get_string('pluginname', 'local_qtracker'), + null, \navigation_node::TYPE_CONTAINER, null, 'qtracker' +); +$issuesnode->add( + get_string('issues', 'local_qtracker'), + new \moodle_url('/local/qtracker/view.php', array('courseid' => $courseid)) +); +$issuesnode->add(get_string('newissue', 'local_qtracker')); + +// Process form actions. +$mform = new new_issue_form($PAGE->url); + +if ($mform->is_cancelled()) { + redirect($returnurl); + +} else if ($data = $mform->get_data()) { + $context = \context_course::instance($courseid); + $question = \question_bank::load_question($data->questionid); + question_require_capability_on($question, 'view'); + + $issue = issue::create($data->title, $data->description['text'], $question, $context->id); + $issue->open(); + $issueurl = new \moodle_url('/local/qtracker/issue.php', array('courseid' => $courseid, 'issueid' => $issue->get_id())); + redirect($issueurl); +} + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('createnewissue', 'local_qtracker')); +$mform->display(); +echo $OUTPUT->footer(); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..a80465d --- /dev/null +++ b/styles.css @@ -0,0 +1,161 @@ +#qtracker-sidebar { + width: 100%; + z-index: 30; + border: 1px solid #dee2e6; + background-color: #fff; +} + +.qtracker-sidebar-header { + padding: .75rem; + font-size: 1em; + top: 0; +} + +.qtracker-sidebar-footer { + padding: .75rem; + font-size: 1em; + bottom: 0; +} + +.qtracker-action-item { + line-height: 2.5em; +} + +.qtracker-relative { + position: relative; +} + +.qtracker-bg-blue { + background-color: #1177d1; +} + + +.qtracker-arrow-left::before { + -webkit-clip-path: polygon(0 50%, 100% 0, 100% 100%); + clip-path: polygon(0 50%, 100% 0, 100% 100%); + content: " "; + display: block; + height: 16px; + left: -8px; + pointer-events: none; + position: absolute; + right: 100%; + top: 11px; + width: 8px; + background-color: #dee2e6; +} + +.qtracker-arrow-left-blue::after { + -webkit-clip-path: polygon(0 50%, 100% 0, 100% 100%); + clip-path: polygon(0 50%, 100% 0, 100% 100%); + content: " "; + display: block; + height: 16px; + left: -8px; + pointer-events: none; + position: absolute; + right: 100%; + top: 11px; + width: 8px; + background-color: #1177d1; + margin-left: 1px; +} + +.qtracker-arrow-left-gray::after { + -webkit-clip-path: polygon(0 50%, 100% 0, 100% 100%); + clip-path: polygon(0 50%, 100% 0, 100% 100%); + content: " "; + display: block; + height: 16px; + left: -8px; + pointer-events: none; + position: absolute; + right: 100%; + top: 11px; + width: 8px; + background-color: #fff; + margin-left: 1px; +} + +.qtracker-container { + position: relative; + min-height: 400px; +} + +.qtracker-container .icon { + height: 20px; + font-size: 20px; + width: auto; +} + +.qtracker-push-pane-over { + padding-right: 0; +} + +#questions-table-wrapper { + position: relative; + min-height: 360px; +} + +.questions-table { + width: 100%; +} + +.flex-grow { + flex: 1 0 auto; +} + +.resizer { + border-left: 1px solid #dee2e6; + width: 5px; + height: 100%; + background-color: #dee2e6; + position: absolute; + right: 0; + bottom: 0; + cursor: w-resize; +} + +.questiontext { + position: relative; + zoom: 1; + padding-left: .3em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +@media (min-width: 768px) { + #qtracker-sidebar { + position: absolute; + top: calc(-1.25rem - 2px); + height: calc(100% + 2 * 1.25rem + 2px); + box-shadow: -3px 0 5px rgba(36, 41, 46, .05); + width: calc(40% - -1.25rem); + z-index: 30; + animation: show-pane .2s cubic-bezier(0, 0, 0, 1); + } + .qtracker-sidebar-left { + left: calc(-1.25rem); + border: unset; + border-right: 1px solid #dee2e6; + } + .qtracker-sidebar-right { + right: calc(-1.25rem); + border: unset; + border-left: 1px solid #dee2e6; + } + .qtracker-push-pane-over { + /* padding-right: 40%; */ + transition: padding 0.2s; + } +} + +@keyframes show-pane { + from { + right: calc(-400px - 1.25rem); + } + to { + right: -1.25rem; + } +} diff --git a/templates/aside_block.mustache b/templates/aside_block.mustache new file mode 100644 index 0000000..f513bd6 --- /dev/null +++ b/templates/aside_block.mustache @@ -0,0 +1,40 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/aside_block + + aside block template. + + Example context (json): + { + "label": "tags", + "items": [{text:"An action", state: "disabled"}, {text:"another action", state: "active"}, {text:"a third action", state: "active"} ], + } +}} + +
+
+
{{label}}
+ {{> local_qtracker/dropdown}} +
+ +
+ {{#items}} +
{{{text}}}
+ {{/items}} +
+
diff --git a/templates/button.mustache b/templates/button.mustache new file mode 100644 index 0000000..81bbcd5 --- /dev/null +++ b/templates/button.mustache @@ -0,0 +1,41 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/button + + Button template. + + Example context (json): + { + "classes": "", + "type": "submit", + "primary": true, + "tooltip": "A button.", + "label": "Button", + "disabled": false, + "id": "SomeId", + "name": "SomeName" + } +}} +
+ +
diff --git a/templates/dropdown.mustache b/templates/dropdown.mustache new file mode 100644 index 0000000..66cf754 --- /dev/null +++ b/templates/dropdown.mustache @@ -0,0 +1,71 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/aside_block + + aside block template. + + Example context (json): + { + "header": "Dropdown header", + "search": true, + "label": "tags", + "trigger": "tags", + "items": [{text:"An action", state: "disabled"}, {text:"another action", state: "active"}, {text:"a third action", state: "active"} ], + } +}} + diff --git a/templates/email_comment_html.mustache b/templates/email_comment_html.mustache new file mode 100644 index 0000000..8c0e582 --- /dev/null +++ b/templates/email_comment_html.mustache @@ -0,0 +1,141 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/email_comment_html + + Template which defines a comment for sending in a HTML email. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Example context (json): + { + "coursename": "Math 101", + "issuetitle": "Incorrect comma", + "issuedescription": "The comma used should be ',' and not '.'", + "authorfullname": "André Storhaug", + "commentdescription": "This needs to be corrected onn all questions." + "commentdate": "29.07.2021" + } +}} + + + + + + + + + + + + + + + + +
+   + +

+ {{{ issuetitle }}} +

+
+ {{{ issuedescription }}} +
+ +
+ +
+
+ {{{ authorpicture }}} + + +
+ {{{ commentdate }}} +
+
+ {{{ commentdescription }}} +
+
+ diff --git a/templates/email_comment_text.mustache b/templates/email_comment_text.mustache new file mode 100644 index 0000000..5df3271 --- /dev/null +++ b/templates/email_comment_text.mustache @@ -0,0 +1,48 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/email_comment_text + + Template which defines a comment for sending in a HTML email. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Example context (json): + { + "coursename": "Math 101", + "issuetitle": "Incorrect comma", + "issuedescription": "The comma used should be ',' and not '.'", + "authorfullname": "André Storhaug", + "commentdescription": "This needs to be corrected onn all questions." + "commentdate": "29.07.2021" + } +}} + +{{coursename}} + + +{{issuetitle}} +{{issuedescription}} + + +{{authorfullname}} +{{commentdescription}} +{{commentdate}} diff --git a/templates/issue_comment.mustache b/templates/issue_comment.mustache new file mode 100644 index 0000000..fa1be5d --- /dev/null +++ b/templates/issue_comment.mustache @@ -0,0 +1,65 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/issue_state_badge + + Issue state badge template. + + Example context (json): + { + "new": true, + "profileimageurl": "https://moodle.org/pix/u/f3.png", + "action": "someAction" + } +}} +
+
+ User picture +
+
+ {{fullname}}  +
+ {{#str}} commentedon, local_qtracker {{/str}} + {{#userdate}} {{timecreated}}, {{#str}} strftimedate, core_langconfig {{/str}} {{/userdate}} +
+
+ {{#mailed}} + + {{# pix }} t/message, core {{/ pix }} + + {{/mailed}} + {{^mailed}} + + {{# pix }} t/message, core {{/ pix }} + + {{/mailed}} + + {{# pix }} t/delete, core {{/ pix }} + +
+
+
+ {{{description}}} +
+
+
+
+{{#js}} +require(['jquery', 'local_qtracker/issue_comment_controls'], function($, IssueCommentControls) { + new IssueCommentControls({{id}}); +}); +{{/js}} diff --git a/templates/issue_description.mustache b/templates/issue_description.mustache new file mode 100644 index 0000000..ca8ba53 --- /dev/null +++ b/templates/issue_description.mustache @@ -0,0 +1,43 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/issue_description + + Issue description template. + + Example context (json): + { + "profileimageurl": "https://moodle.org/pix/u/f3.png" + } +}} +
+ User picture + +
+
+ {{fullname}}  +
+ {{#str}} openedissueon, local_qtracker {{/str}} + {{#userdate}} {{timecreated}}, {{#str}} strftimedate, core_langconfig {{/str}} {{/userdate}} +
+
+
+
+ {{{description}}} +
+
+
diff --git a/templates/issue_registration_block.mustache b/templates/issue_registration_block.mustache new file mode 100644 index 0000000..9d9e981 --- /dev/null +++ b/templates/issue_registration_block.mustache @@ -0,0 +1,83 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/issue_registration_block + + Issue registration block template. + + Context variables required for this template: + * action - form element name + * name - element id + * qubaid - question bank id + * options - list of options containing name, value, selected + + For a full list of the context for this template see the course_competencies_page renderable + + Example context (json): + { + "action": "https://domain.example/file.php", + "name": "test", + "hasmultiple": true, + "select": { + "id": "someId", + "name": "questionid", + "options": [ + { "name": "Option 1", "value": "1", "selected": true }, + { "name": "Option 2", "value": "2", "selected": false } + ] + }, + "qubaid": "10", + "uniqid": "1" + } +}} +
+
+ {{#hasmultiple}} + {{#select}} +
+ {{> local_qtracker/select }} +
+ {{/select}} + {{/hasmultiple}} + {{^hasmultiple}} + + {{/hasmultiple}} + +
+ +
+ {{#str}} validtitle, local_qtracker {{/str}} +
+
+
+ +
+ {{#str}} validdescription, local_qtracker {{/str}} +
+
+ {{#button}} +
+ {{>local_qtracker/button}} +
+ {{/button}} +
+
+{{#js}} +require(['jquery', 'local_qtracker/block_form_manager'], function($, BlockFormManager) { + BlockFormManager.init('[data-action=newissue]', '{{{issueids}}}', {{contextid}}); +}); +{{/js}} diff --git a/templates/issue_state_badge.mustache b/templates/issue_state_badge.mustache new file mode 100644 index 0000000..db156d5 --- /dev/null +++ b/templates/issue_state_badge.mustache @@ -0,0 +1,37 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/issue_state_badge + + Issue state badge template. + + Example context (json): + { + "new": true + } +}} + +{{#new}} + {{#str}} new, local_qtracker {{/str}} +{{/new}} +{{#open}} + {{#str}} open, local_qtracker {{/str}} +{{/open}} +{{#closed}} + {{#str}} closed, local_qtracker {{/str}} +{{/closed}} + diff --git a/templates/question_issue_page.mustache b/templates/question_issue_page.mustache new file mode 100644 index 0000000..5867abe --- /dev/null +++ b/templates/question_issue_page.mustache @@ -0,0 +1,166 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/questions_issue_page + + Question issue page template. + + Example context (json): + { + "questionissue": { + "id": "2", + "title": "Issue title", + "description": "Issue description" + }, + "action": "someAction", + "profileimageurl": "https://moodle.org/pix/u/f3.png" + } +}} + +
+
+ {{#questionissue}} +
+ +
+
+

+
{{title}} #{{id}}
+ +

+
+
+ {{#edittitlebutton}} + {{> local_qtracker/button}} + {{/edittitlebutton}} + {{#newissuebutton}} +
+ {{> local_qtracker/button}} +
+ {{/newissuebutton}} +
+
+
+
+
+

+ {{>local_qtracker/issue_state_badge}} +

+ {{#issuedescription}} + {{fullname}}  +
+ {{#str}} openedissueon, local_qtracker {{/str}} + {{#userdate}} {{timecreated}}, {{#str}} strftimedate, core_langconfig {{/str}} {{/userdate}} +
+
+ {{/issuedescription}} +
+
+
+ + {{/questionissue}} + + +
+ {{#question}} + {{! Question data here }} +
+
+
{{{questiontext}}}
+
+
+
{{#str}} actions, core {{/str}}
+ + +
+
+ {{/question}} + + {{#questionissue}} +
+
+
+ {{#issuedescription}} + {{> local_qtracker/issue_description}} + {{/issuedescription}} + {{#comments}} +
+ {{> local_qtracker/issue_comment}} +
+ {{/comments}} +
+
+
+ Profile image +
+
+ +
+
+
+
+
+ + +
+
+
+ {{#closebutton}} + {{> local_qtracker/button}} + {{/closebutton}} + {{#reopenbutton}} + {{> local_qtracker/button}} + {{/reopenbutton}} +
+ {{#commentbutton}} + {{> local_qtracker/button}} + {{/commentbutton}} +
+
+
+
+
+
+
+ {{#asideblocks}} +
+ {{> local_qtracker/aside_block}} +
+
+ {{/asideblocks}} +
+
+
+
+
+ {{/questionissue}} +
+{{#js}} +require(['jquery', 'local_qtracker/question_issue_page'], function($, QuestionIssuePage) { + new QuestionIssuePage({{courseid}}, {{question.questionid}}, {{questionissue.id}}); +}); +{{/js}} diff --git a/templates/question_issues_page.mustache b/templates/question_issues_page.mustache new file mode 100644 index 0000000..f63f018 --- /dev/null +++ b/templates/question_issues_page.mustache @@ -0,0 +1,29 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/question_issues_page + + Question issues page template + + Example context (json): + { + "questionissues": "raw html" + } +}} +
+ {{{questionissues}}} +
diff --git a/templates/questions_page.mustache b/templates/questions_page.mustache new file mode 100644 index 0000000..28aff7c --- /dev/null +++ b/templates/questions_page.mustache @@ -0,0 +1,58 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/questions_page + + Questions page template + + Example context (json): + { + "questions": "raw html" + } +}} +
+ +
+
+

{{#str}} questionissues, local_qtracker {{/str}}

+
+
+ {{#allissuesbutton}} +
+ {{> local_qtracker/button}} +
+ {{/allissuesbutton}} + {{#newissuebutton}} +
+ {{> local_qtracker/button}} +
+ {{/newissuebutton}} +
+
+
+
+
+
+ {{{questions}}} +
+
+
+{{#js}} +require(['jquery', 'local_qtracker/questions_table_page'], function($, QuestionsTablPage) { + new QuestionsTablPage({{courseid}}); +}); +{{/js}} diff --git a/templates/select.mustache b/templates/select.mustache new file mode 100644 index 0000000..20fb1c9 --- /dev/null +++ b/templates/select.mustache @@ -0,0 +1,55 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/select + + Select template. + + Example context (json): + { + "name": "test", + "id": "test0", + "options": [ + { "name": "Option 1", "value": "V", "selected": true }, + { "name": "Option 2", "value": "V", "selected": true } + ] + } +}} +
+ {{#label}} + + {{/label}} + {{#helpicon}} + {{>core/help_icon}} + {{/helpicon}} + +
diff --git a/templates/sidebar.mustache b/templates/sidebar.mustache new file mode 100644 index 0000000..f65b2c6 --- /dev/null +++ b/templates/sidebar.mustache @@ -0,0 +1,62 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/sidebar + + Sidebar template. + + Example context (json): + { + } +}} +
+
+
+
+ {{questiontitle}} #{{questionid}} +
+ {{#options}} + {{>local_qtracker/dropdown}} + {{/options}} + {{#close}} + + {{>core/pix_icon_fontawesome}} + + {{/close}} +
+
+
+
+
+
+ {{>core/loading}} +
+
+
+
+ +
+
diff --git a/templates/sidebar_item_issue.mustache b/templates/sidebar_item_issue.mustache new file mode 100644 index 0000000..c0cef0a --- /dev/null +++ b/templates/sidebar_item_issue.mustache @@ -0,0 +1,61 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_qtracker/sidebar_item_issue + + Issue sidebar item template. + + Example context (json): + { + "profileimageurl": "https://moodle.org/pix/u/f3.png", + "fullname": "André Storhaug", + "userurl": "https://example.com/user/profile.php?id=3", + "timecreated": "1587655101", + "id": "Issue id", + "title": "Issue title", + "description": "Issue description", + "extraclasses": "acctive", + "actions": [ + { "name": "Close", "action": "V", "disabled": false }, + { "name": "Subsume", "action": "V", "disabled": true }, + ] + } +}} + diff --git a/version.php b/version.php new file mode 100755 index 0000000..caf94fe --- /dev/null +++ b/version.php @@ -0,0 +1,33 @@ +. + +/** + * Version data + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2021 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2021072200; +$plugin->requires = 2016120500; +$plugin->cron = 0; +$plugin->component = 'local_qtracker'; +$plugin->maturity = MATURITY_BETA; +$plugin->release = '0.1.0'; diff --git a/view.php b/view.php new file mode 100644 index 0000000..94106b5 --- /dev/null +++ b/view.php @@ -0,0 +1,59 @@ +. + +/** + * Display qtracker question page + * + * @package local_qtracker + * @author André Storhaug + * @copyright 2020 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_qtracker; + +require_once('../../config.php'); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +global $DB, $OUTPUT, $PAGE; + +// Check for all required variables. +$courseid = required_param('courseid', PARAM_INT); +$manuallySorted = isset($_GET['tsort']); + +if (!$course = $DB->get_record('course', array('id' => $courseid))) { + print_error('invalidcourseid'); +} +$context = \context_course::instance($course->id); + +require_login($course); +require_capability('local/qtracker:viewall', $context); + +$url = new \moodle_url('/local/qtracker/view.php', array('courseid' => $courseid)); + +$PAGE->set_url($url); +$PAGE->set_pagelayout('incourse'); +$PAGE->set_heading(get_string('pluginname', 'local_qtracker')); + +echo $OUTPUT->header(); + +// Get table renderer and display table. +$table = new \local_qtracker\output\questions_table(uniqid(), $url, $context, $manuallySorted); +$renderer = $PAGE->get_renderer('local_qtracker'); +$questionspage = new \local_qtracker\output\questions_page($table, $courseid); +echo $renderer->render($questionspage); + +echo $OUTPUT->footer();