From f2637f607349f8120238db52aab7f787d09ac99a Mon Sep 17 00:00:00 2001 From: David Rise Knotten Date: Tue, 16 Mar 2021 19:25:21 +0100 Subject: [PATCH 01/60] Changed default sort order to new, descending --- classes/output/questions_table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/output/questions_table.php b/classes/output/questions_table.php index 50d1c68..030820e 100644 --- a/classes/output/questions_table.php +++ b/classes/output/questions_table.php @@ -143,7 +143,7 @@ protected function define_table_columns() { */ protected function define_table_configs() { $this->collapsible(false); - $this->sortable(true); + $this->sortable(true, get_string('new', 'local_qtracker'), SORT_DESC); $this->pageable(true); } From ecee32754e550cb2cf9a86bc2e9feca09d9b420e Mon Sep 17 00:00:00 2001 From: David Rise Knotten Date: Tue, 16 Mar 2021 22:51:29 +0100 Subject: [PATCH 02/60] Created sort order new>open>closed>questionid on question issue table --- classes/output/questions_table.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/classes/output/questions_table.php b/classes/output/questions_table.php index 030820e..3b84705 100644 --- a/classes/output/questions_table.php +++ b/classes/output/questions_table.php @@ -143,8 +143,24 @@ protected function define_table_columns() { */ protected function define_table_configs() { $this->collapsible(false); - $this->sortable(true, get_string('new', 'local_qtracker'), SORT_DESC); + $this->sortable(true); $this->pageable(true); + $sortdata = [ + [ + 'sortby' => 'new', + 'sortorder' => SORT_DESC, + ], [ + 'sortby' => 'open', + 'sortorder' => SORT_DESC, + ], [ + 'sortby' => 'closed', + 'sortorder' => SORT_DESC, + ], [ + 'sortby' => 'id', + 'sortorder' => SORT_ASC, + ] + ]; + $this->set_sortdata($sortdata); } /** From 1aafefa03ad060af9faf1eb3f4da9d9f8d4aa5e0 Mon Sep 17 00:00:00 2001 From: David Rise Knotten Date: Sun, 28 Mar 2021 17:55:57 +0200 Subject: [PATCH 03/60] Changed issue creator url to refer to users page --- classes/output/question_issue_page.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/classes/output/question_issue_page.php b/classes/output/question_issue_page.php index 01f5d70..e31cbba 100644 --- a/classes/output/question_issue_page.php +++ b/classes/output/question_issue_page.php @@ -90,7 +90,10 @@ public function export_for_template(renderer_base $output) { $issuedescription = new stdClass(); $user = $DB->get_record('user', array('id' => $issuedetails->userid)); $issuedescription->fullname = $user->username; - $issuedescription->userurl = "http://lol.no"; + $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); From f9e185d675aab953002796125eb7737de5efaccf Mon Sep 17 00:00:00 2001 From: David Rise Knotten Date: Sun, 28 Mar 2021 18:13:29 +0200 Subject: [PATCH 04/60] Fixing faulty JSON --- templates/issue_comment.mustache | 4 ++-- templates/question_issue_page.mustache | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/issue_comment.mustache b/templates/issue_comment.mustache index c965473..79a2013 100644 --- a/templates/issue_comment.mustache +++ b/templates/issue_comment.mustache @@ -22,8 +22,8 @@ Example context (json): { "new": true, - "profileimageurl": "https://moodle.org/pix/u/f3.png" - "action": someAction + "profileimageurl": "https://moodle.org/pix/u/f3.png", + "action": "someAction" } }}
diff --git a/templates/question_issue_page.mustache b/templates/question_issue_page.mustache index 9d3499b..8c9139f 100644 --- a/templates/question_issue_page.mustache +++ b/templates/question_issue_page.mustache @@ -26,7 +26,7 @@ "title": "Issue title", "description": "Issue description" }, - "action": someAction, + "action": "someAction", "profileimageurl": "https://moodle.org/pix/u/f3.png" } }} From db5d45134adfec9b6b5fa6b893c3a56ef15a019e Mon Sep 17 00:00:00 2001 From: David Rise Knotten Date: Sun, 11 Apr 2021 14:22:42 +0200 Subject: [PATCH 05/60] Added a very basic method to send a comment as mail --- classes/output/question_issue_page.php | 8 ++++++++ issue.php | 15 ++++++++++++++- lang/en/local_qtracker.php | 4 ++++ templates/question_issue_page.mustache | 3 +++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/classes/output/question_issue_page.php b/classes/output/question_issue_page.php index 01f5d70..bd294fe 100644 --- a/classes/output/question_issue_page.php +++ b/classes/output/question_issue_page.php @@ -147,6 +147,14 @@ public function export_for_template(renderer_base $output) { $data->closebutton = $closebutton; } + + $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"; diff --git a/issue.php b/issue.php index 85585ec..bddf184 100644 --- a/issue.php +++ b/issue.php @@ -27,11 +27,12 @@ use local_qtracker\issue; use local_qtracker\output\question_issue_page; +use mod_capquiz\capquiz; require_once('../../config.php'); require_once($CFG->dirroot . '/local/qtracker/lib.php'); -global $DB, $OUTPUT, $PAGE; +global $DB, $OUTPUT, $PAGE, $USER; // Check for all required variables. $courseid = required_param('courseid', PARAM_INT); @@ -87,6 +88,18 @@ redirect($PAGE->url); } +$commentandmailissue = optional_param('commentandmailissue', false, PARAM_BOOL); +if ($commentandmailissue) { + //TODO implement mailing function + $user = $DB->get_record('user', array('id' => $issue->get_userid())); + if(email_to_user($user, $USER,"subjecttext", $commenttext)) { + echo '

Successfully sent mail

'; + } else { + echo '

Failed to send mail

'; + } + //get_string('issuesubject', 'local_qtracker') +} + $closeissue = optional_param('closeissue', false, PARAM_BOOL); if ($closeissue) { if ($commenttext != false) { diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php index 1e7e4b8..5e605b5 100755 --- a/lang/en/local_qtracker.php +++ b/lang/en/local_qtracker.php @@ -83,6 +83,10 @@ $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['confirm'] = 'Confirm'; $string['deletecomment'] = 'Delete comment'; $string['confirmdeletecomment'] = 'Are you sure you want to delete this comment?'; diff --git a/templates/question_issue_page.mustache b/templates/question_issue_page.mustache index 9d3499b..d19decc 100644 --- a/templates/question_issue_page.mustache +++ b/templates/question_issue_page.mustache @@ -95,6 +95,9 @@
+ {{#commentandmailbutton}} + {{> local_qtracker/button}} + {{/commentandmailbutton}}
{{#closebutton}} {{> local_qtracker/button}} From 07395de46e7c0fd4f6facba30d8946f3779664bf Mon Sep 17 00:00:00 2001 From: David Rise Knotten Date: Mon, 12 Apr 2021 20:25:22 +0200 Subject: [PATCH 06/60] Added moodle notification responses for issues --- classes/output/question_issue_page.php | 6 ++++++ issue.php | 28 ++++++++++++++++++++++++-- lang/en/local_qtracker.php | 2 ++ templates/question_issue_page.mustache | 13 +++++++++--- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/classes/output/question_issue_page.php b/classes/output/question_issue_page.php index bd294fe..da32580 100644 --- a/classes/output/question_issue_page.php +++ b/classes/output/question_issue_page.php @@ -147,6 +147,12 @@ public function export_for_template(renderer_base $output) { $data->closebutton = $closebutton; } + $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; diff --git a/issue.php b/issue.php index bddf184..616e1dc 100644 --- a/issue.php +++ b/issue.php @@ -25,6 +25,8 @@ namespace local_qtracker; +use core\check\performance\debugging; +use core\message\message; use local_qtracker\issue; use local_qtracker\output\question_issue_page; use mod_capquiz\capquiz; @@ -88,16 +90,38 @@ redirect($PAGE->url); } +$commentanddmissue = optional_param('commentanddmissue', false,PARAM_BOOL); +if ($commentanddmissue) { + $user = $DB->get_record('user', array('id' => $issue->get_userid())); + $message = new \core\message\message(); + $message->component = 'moodle'; // Your plugin's name + $message->name = 'instantmessage'; // Your notification name from message.php + $message->userfrom = $USER; // If the message is 'from' a specific user you can set them here + $message->userto = $user; + $message->subject = 'Issue ' . $issue->get_title(); + $message->fullmessage = $commenttext; + $message->fullmessageformat = FORMAT_MARKDOWN; + $message->fullmessagehtml = '

message body

'; + $message->smallmessage = 'small message'; + $message->notification = 1; // Because this is a notification generated from Moodle, not a user-to-user message + $message->contexturl = (new \moodle_url('/course/'))->out(false); // A relevant URL for the notification + $message->contexturlname = 'Course list'; // Link title explaining where users get to for the contexturl + $content = array('*' => array('header' => ' test ', 'footer' => ' test ')); // Extra content for specific processor + $message->set_additional_content('email', $content); + //TODO create message from teacher to student + message_send($message); +} + $commentandmailissue = optional_param('commentandmailissue', false, PARAM_BOOL); if ($commentandmailissue) { //TODO implement mailing function $user = $DB->get_record('user', array('id' => $issue->get_userid())); - if(email_to_user($user, $USER,"subjecttext", $commenttext)) { + if(email_to_user($user, $USER,get_string('issuesubject', 'local_qtracker'), $commenttext)) { echo '

Successfully sent mail

'; + debugging('Sending mail to ' . $user->email . ' from ' . $USER->email); } else { echo '

Failed to send mail

'; } - //get_string('issuesubject', 'local_qtracker') } $closeissue = optional_param('closeissue', false, PARAM_BOOL); diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php index 5e605b5..93d3d6d 100755 --- a/lang/en/local_qtracker.php +++ b/lang/en/local_qtracker.php @@ -86,6 +86,8 @@ //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['confirm'] = 'Confirm'; $string['deletecomment'] = 'Delete comment'; diff --git a/templates/question_issue_page.mustache b/templates/question_issue_page.mustache index d19decc..35e8dd3 100644 --- a/templates/question_issue_page.mustache +++ b/templates/question_issue_page.mustache @@ -95,9 +95,16 @@
- {{#commentandmailbutton}} - {{> local_qtracker/button}} - {{/commentandmailbutton}} +
+ {{#commentandmailbutton}} + {{> local_qtracker/button}} + {{/commentandmailbutton}} +
+
+ {{#commentanddmbutton}} + {{> local_qtracker/button}} + {{/commentanddmbutton}} +
{{#closebutton}} {{> local_qtracker/button}} From b3dfba5c09342508e10d8594e0d9413c2d62b112 Mon Sep 17 00:00:00 2001 From: David Rise Knotten Date: Mon, 12 Apr 2021 20:29:37 +0200 Subject: [PATCH 07/60] Applying requested changes Checked whether the removal of in issues_pane_item template was correct, Found no usages --- classes/external/delete_issue.php | 1 - templates/issues_pane_item.mustache | 4 ++-- templates/select.mustache | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/classes/external/delete_issue.php b/classes/external/delete_issue.php index 0e238cf..62536d4 100644 --- a/classes/external/delete_issue.php +++ b/classes/external/delete_issue.php @@ -37,7 +37,6 @@ use external_single_structure; use external_warnings; use local_qtracker\issue; -use mysql_xdevapi\Result; /** * Class delete_issue diff --git a/templates/issues_pane_item.mustache b/templates/issues_pane_item.mustache index b9979f9..2cb48dd 100644 --- a/templates/issues_pane_item.mustache +++ b/templates/issues_pane_item.mustache @@ -45,6 +45,6 @@
- {{title}} -
{{description}}
+ {{title}} +
{{description}}
diff --git a/templates/select.mustache b/templates/select.mustache index 93e6a41..20fb1c9 100644 --- a/templates/select.mustache +++ b/templates/select.mustache @@ -38,7 +38,7 @@ {{#helpicon}} {{>core/help_icon}} {{/helpicon}} - {{#options}} {{#optgroup}} From c949f6c0420e32ddee983362bb960f1f1e3d5d7e Mon Sep 17 00:00:00 2001 From: David Rise Knotten Date: Mon, 12 Apr 2021 20:46:31 +0200 Subject: [PATCH 08/60] Fixed bug blocking users from overriding sorting Also changed default sort order for question id --- classes/output/questions_table.php | 42 ++++++++++++++++-------------- view.php | 3 ++- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/classes/output/questions_table.php b/classes/output/questions_table.php index 3b84705..f4fa068 100644 --- a/classes/output/questions_table.php +++ b/classes/output/questions_table.php @@ -48,8 +48,9 @@ class questions_table extends table_sql { * @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) { + public function __construct($uniqueid, $url, $context, $manuallySorted = false) { global $CFG; parent::__construct($uniqueid); // TODO: determine which context to use... @@ -60,7 +61,7 @@ public function __construct($uniqueid, $url, $context) { // Set the baseurl. $this->define_baseurl($url); // Define configs. - $this->define_table_configs(); + $this->define_table_configs($manuallySorted); // Define SQL. $this->setup_sql_queries(); } @@ -140,27 +141,30 @@ protected function define_table_columns() { /** * Define table configs. + * @param boolean $manuallySorted if false the default sorting will be used */ - protected function define_table_configs() { + protected function define_table_configs($manuallySorted) { $this->collapsible(false); $this->sortable(true); $this->pageable(true); - $sortdata = [ - [ - 'sortby' => 'new', - 'sortorder' => SORT_DESC, - ], [ - 'sortby' => 'open', - 'sortorder' => SORT_DESC, - ], [ - 'sortby' => 'closed', - 'sortorder' => SORT_DESC, - ], [ - 'sortby' => 'id', - 'sortorder' => SORT_ASC, - ] - ]; - $this->set_sortdata($sortdata); + 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); + } } /** diff --git a/view.php b/view.php index 53be5fd..94106b5 100644 --- a/view.php +++ b/view.php @@ -32,6 +32,7 @@ // 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'); @@ -50,7 +51,7 @@ echo $OUTPUT->header(); // Get table renderer and display table. -$table = new \local_qtracker\output\questions_table(uniqid(), $url, $context); +$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); From a355e1f1df51df165280d07a6e9c8c873b34f661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Storhaug?= Date: Thu, 10 Jun 2021 17:34:39 +0200 Subject: [PATCH 09/60] Add Coveralls parallel run --- .github/workflows/moodle-ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index f090810..50f9d58 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -102,3 +102,29 @@ jobs: - 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 From 3c4100b001c8a1dd36b5c6dbf83c405cd6fecaf3 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 11 Jun 2021 12:30:07 +0200 Subject: [PATCH 10/60] Adding messageprovider for issue responses --- db/messages.php | 10 ++++++++++ issue.php | 14 ++++++++------ lang/en/local_qtracker.php | 1 + 3 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 db/messages.php diff --git a/db/messages.php b/db/messages.php new file mode 100644 index 0000000..7712870 --- /dev/null +++ b/db/messages.php @@ -0,0 +1,10 @@ + array ( + 'default' => array ( + 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN, + 'email' => MESSAGE_DISALLOWED + ) + ) +); diff --git a/issue.php b/issue.php index 616e1dc..b35481a 100644 --- a/issue.php +++ b/issue.php @@ -94,20 +94,22 @@ if ($commentanddmissue) { $user = $DB->get_record('user', array('id' => $issue->get_userid())); $message = new \core\message\message(); - $message->component = 'moodle'; // Your plugin's name - $message->name = 'instantmessage'; // Your notification name from message.php + $message->component = 'qtracker'; // Your plugin's name + $message->name = 'issueresponse'; // Your notification name from message.php $message->userfrom = $USER; // If the message is 'from' a specific user you can set them here $message->userto = $user; - $message->subject = 'Issue ' . $issue->get_title(); + $message->subject = 'Issue '.$issue->get_title(); $message->fullmessage = $commenttext; $message->fullmessageformat = FORMAT_MARKDOWN; - $message->fullmessagehtml = '

message body

'; - $message->smallmessage = 'small message'; + $message->fullmessagehtml = '

'.$commenttext.'

'; + $message->smallmessage = ''; $message->notification = 1; // Because this is a notification generated from Moodle, not a user-to-user message $message->contexturl = (new \moodle_url('/course/'))->out(false); // A relevant URL for the notification $message->contexturlname = 'Course list'; // Link title explaining where users get to for the contexturl + /* $content = array('*' => array('header' => ' test ', 'footer' => ' test ')); // Extra content for specific processor - $message->set_additional_content('email', $content); + $message->set_additional_content('popup', $content); + */ //TODO create message from teacher to student message_send($message); } diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php index 93d3d6d..1468aa4 100755 --- a/lang/en/local_qtracker.php +++ b/lang/en/local_qtracker.php @@ -87,6 +87,7 @@ $string['commentandmail'] = 'Comment and forward mail to issue creator'; $string['issuesubject'] = 'some subject'; $string['commentanddm'] = 'Comment and dm issue to creator'; +$string['nessageorivuder:issueresponse'] = 'Response from teacher on student issue'; $string['confirm'] = 'Confirm'; From 886e668a76a266ef5e22043543165b23d8fd1924 Mon Sep 17 00:00:00 2001 From: David Rise Knotten Date: Sat, 12 Jun 2021 13:24:51 +0200 Subject: [PATCH 11/60] Quick push --- db/messages.php | 5 +++-- lang/en/local_qtracker.php | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/db/messages.php b/db/messages.php index 7712870..f14d2a5 100644 --- a/db/messages.php +++ b/db/messages.php @@ -1,10 +1,11 @@ array ( 'default' => array ( - 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN, - 'email' => MESSAGE_DISALLOWED + 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF + MESSAGE_DEFAULT_LOGGEDIN ) ) ); diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php index 1468aa4..52533eb 100755 --- a/lang/en/local_qtracker.php +++ b/lang/en/local_qtracker.php @@ -87,7 +87,7 @@ $string['commentandmail'] = 'Comment and forward mail to issue creator'; $string['issuesubject'] = 'some subject'; $string['commentanddm'] = 'Comment and dm issue to creator'; -$string['nessageorivuder:issueresponse'] = 'Response from teacher on student issue'; +$string['messageprovider:issueresponse'] = 'Response from teacher on student issue'; $string['confirm'] = 'Confirm'; From 8ffdbc075ff7b413fbcbfacaeb37e4f77e65b7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Storhaug?= Date: Sat, 12 Jun 2021 16:15:19 +0200 Subject: [PATCH 12/60] Add initial privacy interface --- .gitignore | 2 + classes/privacy/provider.php | 129 +++++++++++++++++++++++++++++++++++ lang/en/local_qtracker.php | 11 +++ 3 files changed, 142 insertions(+) create mode 100644 classes/privacy/provider.php diff --git a/.gitignore b/.gitignore index 233cd7d..c1c0029 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ *.code-workspace .idea/ + +.history diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php new file mode 100644 index 0000000..c782dfe --- /dev/null +++ b/classes/privacy/provider.php @@ -0,0 +1,129 @@ +. + +/** + * 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 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 '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('qtracker_issue', [ + 'userid' => 'privacy:metadata:qtracker_issue:userid', + 'title' => 'privacy:metadata:qtracker_issue:title', + 'description' => 'privacy:metadata:qtracker_issue:description', + 'timecreated' => 'privacy:metadata:qtracker_issue:timecreated' + ], 'privacy:metadata:qtracker_issue'); + + // The table '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('qtracker_comment', [ + 'userid' => 'privacy:metadata:qtracker_comment:userid', + 'description' => 'privacy:metadata:qtracker_comment:description', + 'timecreated' => 'privacy:metadata:qtracker_comment:timecreated' + ], 'privacy:metadata: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 qtracker_issue and qtracker_comment left join? on issueid (comments table) to get all contextids stored in the qtracker_issue table. + } + + /** + * 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) { + // TODO: Export all data from all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). + global $DB; + } + + /** + * 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) { + // TODO: for ALL USERS : delete all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). + global $DB; + } + + /** + * 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) { + // TODO: for one specific user, delete all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). + + global $DB; + + } +} diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php index 1e7e4b8..42e860d 100755 --- a/lang/en/local_qtracker.php +++ b/lang/en/local_qtracker.php @@ -96,3 +96,14 @@ $string['qtracker:editall'] = 'Edit all issues'; $string['qtracker:viewmine'] = 'Edit your own issues'; $string['qtracker:viewall'] = 'View all issues'; + +$string['privacy:metadata:qtracker_issue'] = 'Details about each question issue.'; +$string['privacy:metadata:qtracker_issue:userid'] = 'The user that created the issue.'; +$string['privacy:metadata:qtracker_issue:title'] = 'The title of the issue.'; +$string['privacy:metadata:qtracker_issue:description'] = 'The description of the issue.'; +$string['privacy:metadata:qtracker_issue:timecreated'] = 'The time the issue was created.'; + +$string['privacy:metadata:qtracker_comment'] = 'Details about each question issue comment.'; +$string['privacy:metadata:qtracker_issue:userid'] = 'The user that created the issue comment.'; +$string['privacy:metadata:qtracker_issue:description'] = 'The description of the issue comment.'; +$string['privacy:metadata:qtracker_issue:timecreated'] = 'The time the issue comment was created.'; From 77691dd7357d23e766be7244bd659d3ba05dae50 Mon Sep 17 00:00:00 2001 From: David Rise Knotten Date: Sun, 13 Jun 2021 15:44:38 +0200 Subject: [PATCH 13/60] Qtracker backup --- .../backup_local_qtracker_plugin.class.php | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 backup/moodle2/backup_local_qtracker_plugin.class.php 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..1370f43 --- /dev/null +++ b/backup/moodle2/backup_local_qtracker_plugin.class.php @@ -0,0 +1,71 @@ +. + +/** + * Defines backup_local_qtracker_plugin class + * + * @package local_qtracker + * @author David Rise Knotten + * @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 + * @author David Rise Knotten + * @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 +{ + + /** + * Not used + */ + protected function define_my_settings() { + } + + /** + * Define structure + */ + 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' + ]); + $qtrackercomment = new backup_nested_element('comment', ['id'], [ + 'issueid', 'description', 'userid', 'timecreated' + ]); + + // Build the backup tree + $qtrackerissue->add_child($qtrackercomment); + $pluginwrapper->add_child($qtrackerissue); + $plugin->add_child($pluginwrapper); + + // Define sources + $qtrackerissue->set_source_table('qtracker_issue', ['id' => backup::VAR_MODID]); + $qtrackercomment->set_source_table('qtracker_comment', ['issueid' => backup::VAR_PARENTID]); + + return $plugin; + } +} From 0cff2dd9c798d38fb30d2bbf78def341d0325529 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 14 Jun 2021 16:01:04 +0200 Subject: [PATCH 14/60] Working backup --- .../backup_local_qtracker_plugin.class.php | 22 ++++++---- .../restore_local_qtracker_plugin.class.php | 40 +++++++++++++++++++ lib.php | 15 +++++++ 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 backup/moodle2/restore_local_qtracker_plugin.class.php diff --git a/backup/moodle2/backup_local_qtracker_plugin.class.php b/backup/moodle2/backup_local_qtracker_plugin.class.php index 1370f43..4f13943 100644 --- a/backup/moodle2/backup_local_qtracker_plugin.class.php +++ b/backup/moodle2/backup_local_qtracker_plugin.class.php @@ -33,8 +33,7 @@ * @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 -{ +class backup_local_qtracker_plugin extends backup_local_plugin { /** * Not used @@ -43,18 +42,21 @@ protected function define_my_settings() { } /** - * Define structure + * Define qtracker structure from question entrypoint + * + * @return backup_plugin_element + * @throws base_element_struct_exception */ - protected function define_module_plugin_structure() { + protected function define_question_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', + 'title', 'description', 'questionusageid', 'slot', 'state', 'userid', 'contextid', 'timecreated' ]); $qtrackercomment = new backup_nested_element('comment', ['id'], [ - 'issueid', 'description', 'userid', 'timecreated' + 'description', 'userid', 'timecreated' ]); // Build the backup tree @@ -63,9 +65,15 @@ protected function define_module_plugin_structure() { $plugin->add_child($pluginwrapper); // Define sources - $qtrackerissue->set_source_table('qtracker_issue', ['id' => backup::VAR_MODID]); + $qtrackerissue->set_source_table('qtracker_issue', ['questionid' => backup::VAR_PARENTID]); $qtrackercomment->set_source_table('qtracker_comment', ['issueid' => backup::VAR_PARENTID]); + // Define annotations + // TODO: Make these work + $qtrackerissue->annotate_ids('user','userid'); + $qtrackerissue->annotate_ids('questionusage','questionusageid'); + $qtrackerissue->annotate_ids('context','contextid'); + 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..c7f4d34 --- /dev/null +++ b/backup/moodle2/restore_local_qtracker_plugin.class.php @@ -0,0 +1,40 @@ +. + +/** + * Defines restore_local_qtracker_plugin class + * + * @package local_qtracker + * @author David Rise Knotten + * @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 + * @author David Rise Knotten + * @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 { + +} diff --git a/lib.php b/lib.php index 9d5ef39..1702f64 100644 --- a/lib.php +++ b/lib.php @@ -112,3 +112,18 @@ function issue_require_capability_on($issue, $cap) { } 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; + } +} From 99d9c7fe82565f553fd8a0a568a6d4d275fb4605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Storhaug?= Date: Mon, 14 Jun 2021 18:41:16 +0200 Subject: [PATCH 15/60] Finish privacy implementation --- classes/privacy/provider.php | 136 +++++++++++++++++++++++++++++++++-- lang/en/local_qtracker.php | 1 + version.php | 2 +- 3 files changed, 133 insertions(+), 6 deletions(-) diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index c782dfe..73dc6bc 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -90,6 +90,18 @@ public static function get_metadata(collection $items): collection { public static function get_contexts_for_userid(int $userid): contextlist { // TODO: select all from table qtracker_issue and qtracker_comment left join? on issueid (comments table) to get all contextids stored in the qtracker_issue table. + $sql = "SELECT qi.contextid + FROM {qtracker_issue} qi + LEFT JOIN {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; } /** @@ -101,8 +113,67 @@ public static function get_contexts_for_userid(int $userid): contextlist { * @throws moodle_exception */ public static function export_user_data(approved_contextlist $contextlist) { - // TODO: Export all data from all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). global $DB; + if (empty($contextlist)) { + return; + } + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $sql = "SELECT * FROM {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 {qtracker_issue} qi + INNER JOIN {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); + } } /** @@ -111,8 +182,32 @@ public static function export_user_data(approved_contextlist $contextlist) { * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(context $context) { - // TODO: for ALL USERS : delete all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). global $DB; + $sql = "SELECT qc.id AS id + FROM {qtracker_issue} qi + INNER JOIN {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('qtracker_comment', ['id' => $comment->id]); + } + + $sql = "SELECT id + FROM {qtracker_issue} + WHERE contextid = :contextid"; + $params = [ + 'contextid' => $context->id + ]; + $issues = $DB->get_records_sql($sql, $params); + + foreach ($issues as $issue) { + $DB->delete_records('qtracker_issue', ['id' => $issue->id]); + } } /** @@ -121,9 +216,40 @@ public static function delete_data_for_all_users_in_context(context $context) { * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { - // TODO: for one specific user, delete all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). - 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 {qtracker_issue} qi + INNER JOIN {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('qtracker_comment', ['id' => $comment->id]); + } + + $sql = "SELECT id + FROM {qtracker_issue} + WHERE qc.userid = :userid + AND qi.contextid {$contextsql}"; + $params = [ + 'userid' => $user->id + ]; + $params += $contextparams; + $issues = $DB->get_records_sql($sql, $params); + + foreach ($issues as $issue) { + $DB->delete_records('qtracker_issue', ['id' => $issue->id]); + } } } diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php index 42e860d..9b2da93 100755 --- a/lang/en/local_qtracker.php +++ b/lang/en/local_qtracker.php @@ -64,6 +64,7 @@ $string['issue'] = 'Issue'; $string['issues'] = 'Issues'; +$string['comments'] ='Comments'; $string['submitnewissue'] = 'Submit new issue'; $string['validtitle'] = 'Please provide a valid title.'; diff --git a/version.php b/version.php index a9884ed..f328f98 100755 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2020092200; +$plugin->version = 2021061400; $plugin->requires = 2016120500; $plugin->cron = 0; $plugin->component = 'local_qtracker'; From 5c8ef0e5b1b095d4ddd0e3088890860eb64cdbf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Storhaug?= Date: Mon, 14 Jun 2021 18:42:31 +0200 Subject: [PATCH 16/60] Fix minor sql error --- classes/privacy/provider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 73dc6bc..4c6c51b 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -240,8 +240,8 @@ public static function delete_data_for_user(approved_contextlist $contextlist) { $sql = "SELECT id FROM {qtracker_issue} - WHERE qc.userid = :userid - AND qi.contextid {$contextsql}"; + WHERE userid = :userid + AND contextid {$contextsql}"; $params = [ 'userid' => $user->id ]; From cfdb976787745424110558aaac43fbbda72a263a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Storhaug?= Date: Mon, 14 Jun 2021 18:49:06 +0200 Subject: [PATCH 17/60] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c967865..2798710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ 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] +### Added +- Privacy API is implemented in order to comply with the [General Data Protection Regulation](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation) (GDPR). ## [0.1.0] - YYYY-MM-DD ### Fixed From 0d5f554598b68cb8144ee09dc3c354f91b81a149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Storhaug?= Date: Mon, 14 Jun 2021 19:07:18 +0200 Subject: [PATCH 18/60] Add correct table prefix "local_qtracker" --- classes/event/question_deleted_observer.php | 2 +- classes/external/delete_issue.php | 2 +- classes/external/edit_issue.php | 2 +- classes/external/get_issue.php | 2 +- classes/external/get_issues.php | 2 +- classes/issue.php | 20 ++++++++++---------- classes/issue_comment.php | 10 +++++----- classes/output/issue_registration_block.php | 2 +- classes/output/question_issues_table.php | 2 +- classes/output/questions_table.php | 2 +- db/install.xml | 6 +++--- 11 files changed, 26 insertions(+), 26 deletions(-) diff --git a/classes/event/question_deleted_observer.php b/classes/event/question_deleted_observer.php index 47c0571..f5bd425 100644 --- a/classes/event/question_deleted_observer.php +++ b/classes/event/question_deleted_observer.php @@ -46,7 +46,7 @@ class question_deleted_observer { public static function question_deleted(\core\event\question_deleted $event) { global $DB; // Delete all issues for given question. - $records = $DB->get_records('qtracker_issue', ['questionid' => $event->objectid], '', 'id'); + $records = $DB->get_records('local_qtracker_issue', ['questionid' => $event->objectid], '', 'id'); foreach ($records as $record) { $issue = issue::load($record->id); diff --git a/classes/external/delete_issue.php b/classes/external/delete_issue.php index 62536d4..da271a4 100644 --- a/classes/external/delete_issue.php +++ b/classes/external/delete_issue.php @@ -79,7 +79,7 @@ public static function delete_issue($issueid) { ) ); - if (!$DB->record_exists_select('qtracker_issue', 'id = :issueid AND userid = :userid', + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :issueid AND userid = :userid', array( 'issueid' => $params['issueid'], 'userid' => $USER->id diff --git a/classes/external/edit_issue.php b/classes/external/edit_issue.php index 2bae14f..4cf9e45 100644 --- a/classes/external/edit_issue.php +++ b/classes/external/edit_issue.php @@ -86,7 +86,7 @@ public static function edit_issue($issueid, $issuetitle, $issuedescription) { ) ); - if (!$DB->record_exists_select('qtracker_issue', 'id = :issueid AND userid = :userid', + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :issueid AND userid = :userid', array( 'issueid' => $params['issueid'], 'userid' => $USER->id diff --git a/classes/external/get_issue.php b/classes/external/get_issue.php index 568303d..0453c78 100644 --- a/classes/external/get_issue.php +++ b/classes/external/get_issue.php @@ -81,7 +81,7 @@ public static function get_issue($issueid) { ) ); - if (!$DB->record_exists_select('qtracker_issue', 'id = :issueid AND userid = :userid', + if (!$DB->record_exists_select('local_qtracker_issue', 'id = :issueid AND userid = :userid', array( 'issueid' => $params['issueid'], 'userid' => $USER->id diff --git a/classes/external/get_issues.php b/classes/external/get_issues.php index f8e6889..e7a7e15 100644 --- a/classes/external/get_issues.php +++ b/classes/external/get_issues.php @@ -173,7 +173,7 @@ public static function get_issues($criteria = array()) { } } - $issues = $DB->get_records_select('qtracker_issue', $sql, $sqlparams, 'id ASC'); + $issues = $DB->get_records_select('local_qtracker_issue', $sql, $sqlparams, 'id ASC'); // Finally retrieve each issues information. $returnedissues = array(); diff --git a/classes/issue.php b/classes/issue.php index 4be3fe0..594ccfe 100644 --- a/classes/issue.php +++ b/classes/issue.php @@ -55,7 +55,7 @@ class issue { public function __construct($issue) { global $DB; if (is_scalar($issue)) { - $issue = $DB->get_record('qtracker_issue', array('id' => $issue), '*', MUST_EXIST); + $issue = $DB->get_record('local_qtracker_issue', array('id' => $issue), '*', MUST_EXIST); if (!$issue) { throw new \moodle_exception('errorunexistingmodel', 'analytics', '', $issue); } @@ -185,7 +185,7 @@ public function get_comments() { global $DB; if (empty($this->comments)) { $this->comments = array(); - $comments = $DB->get_records('qtracker_comment', ['issueid' => $this->get_id()]); + $comments = $DB->get_records('local_qtracker_comment', ['issueid' => $this->get_id()]); foreach ($comments as $comment) { array_push($this->comments, new issue_comment($comment)); } @@ -201,7 +201,7 @@ public function get_comments() { */ public static function load(int $issueid) { global $DB; - $issueobj = $DB->get_record('qtracker_issue', ['id' => $issueid]); + $issueobj = $DB->get_record('local_qtracker_issue', ['id' => $issueid]); if ($issueobj === false) { return null; } @@ -237,7 +237,7 @@ public static function create($title, $description, \question_definition $questi // $issueobj->timemodified = $time; // $issueobj->usermodified = $USER->id; - $id = $DB->insert_record('qtracker_issue', $issueobj); + $id = $DB->insert_record('local_qtracker_issue', $issueobj); $issueobj->id = $id; $issue = new issue($issueobj); @@ -252,7 +252,7 @@ public static function create($title, $description, \question_definition $questi public function close() { global $DB; $this->issue->state = "closed"; - $DB->update_record('qtracker_issue', $this->issue); + $DB->update_record('local_qtracker_issue', $this->issue); } /** @@ -263,7 +263,7 @@ public function close() { public function open() { global $DB; $this->issue->state = "open"; - $DB->update_record('qtracker_issue', $this->issue); + $DB->update_record('local_qtracker_issue', $this->issue); } /** @@ -274,7 +274,7 @@ public function open() { public function comment() { $this->comments; - $DB->update_record('qtracker_issue', $this->issue); + $DB->update_record('local_qtracker_issue', $this->issue); } /** @@ -288,7 +288,7 @@ public function delete() { foreach ($comments as $comment) { $comment->delete(); } - return $DB->delete_records('qtracker_issue', array('id' => $this->get_id())); + return $DB->delete_records('local_qtracker_issue', array('id' => $this->get_id())); } /** @@ -299,7 +299,7 @@ public function delete() { public function set_title($title) { global $DB; $this->issue->title = $title; - $DB->update_record('qtracker_issue', $this->issue); + $DB->update_record('local_qtracker_issue', $this->issue); } /** @@ -310,6 +310,6 @@ public function set_title($title) { public function set_description($title) { global $DB; $this->issue->description = $title; - $DB->update_record('qtracker_issue', $this->issue); + $DB->update_record('local_qtracker_issue', $this->issue); } } diff --git a/classes/issue_comment.php b/classes/issue_comment.php index b67277a..060a70b 100644 --- a/classes/issue_comment.php +++ b/classes/issue_comment.php @@ -50,7 +50,7 @@ class issue_comment { public function __construct($comment) { global $DB; if (is_scalar($comment)) { - $comment = $DB->get_record('qtracker_comment', array('id' => $comment), '*', MUST_EXIST); + $comment = $DB->get_record('local_qtracker_comment', array('id' => $comment), '*', MUST_EXIST); if (!$comment) { throw new \moodle_exception('errorunexistingmodel', 'analytics', '', $comment); } @@ -132,7 +132,7 @@ public function get_comments() { */ public static function load(int $comment) { global $DB; - $commentobj = $DB->get_record('qtracker_comment', ['id' => $comment]); + $commentobj = $DB->get_record('local_qtracker_comment', ['id' => $comment]); if ($commentobj === false) { return null; } @@ -158,7 +158,7 @@ public static function create($description, issue $issue) { $commentobj->timecreated = $time; // $commentobj->usermodified = $USER->id; - $id = $DB->insert_record('qtracker_comment', $commentobj); + $id = $DB->insert_record('local_qtracker_comment', $commentobj); $commentobj->id = $id; $comment = new issue_comment($commentobj); @@ -172,7 +172,7 @@ public static function create($description, issue $issue) { */ public function delete() { global $DB; - return $DB->delete_records('qtracker_comment', array('id' => $this->get_id())); + return $DB->delete_records('local_qtracker_comment', array('id' => $this->get_id())); } /** @@ -185,6 +185,6 @@ public function delete() { public function set_description($title) { global $DB; $this->comment->description = $title; - $DB->update_record('qtracker_comment', $this->comment); + $DB->update_record('local_qtracker_comment', $this->comment); } } diff --git a/classes/output/issue_registration_block.php b/classes/output/issue_registration_block.php index 9b84372..33c15cd 100644 --- a/classes/output/issue_registration_block.php +++ b/classes/output/issue_registration_block.php @@ -92,7 +92,7 @@ private function load_issues() { 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('qtracker_issue', 'id', $where, $queryparams); + $this->issueids = $DB->get_fieldset_select('local_qtracker_issue', 'id', $where, $queryparams); } /** diff --git a/classes/output/question_issues_table.php b/classes/output/question_issues_table.php index 55dd205..22cffbc 100644 --- a/classes/output/question_issues_table.php +++ b/classes/output/question_issues_table.php @@ -163,7 +163,7 @@ public function setup_sql_queries() { // TODO: Write SQL to retrieve all rows... $fields = 'DISTINCT'; $fields .= '*'; - $from = '{qtracker_issue} qs'; + $from = '{local_qtracker_issue} qs'; $where = '1=1'; $params = array(); // TODO: find a way to only get the correct contexts.. For now just get everything (keep this empty)... diff --git a/classes/output/questions_table.php b/classes/output/questions_table.php index f4fa068..566c081 100644 --- a/classes/output/questions_table.php +++ b/classes/output/questions_table.php @@ -189,7 +189,7 @@ public function setup_sql_queries() { $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 = '{qtracker_issue} qi'; + $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"; diff --git a/db/install.xml b/db/install.xml index ac93ab7..66f9e08 100755 --- a/db/install.xml +++ b/db/install.xml @@ -4,7 +4,7 @@ xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" > - +
@@ -24,7 +24,7 @@
- +
@@ -34,7 +34,7 @@ - +
From 50d6e809b5fd45ffdd61f93047c05db4feede64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Storhaug?= Date: Sat, 12 Jun 2021 16:15:19 +0200 Subject: [PATCH 19/60] Add initial privacy interface --- .gitignore | 2 + classes/privacy/provider.php | 129 +++++++++++++++++++++++++++++++++++ lang/en/local_qtracker.php | 11 +++ 3 files changed, 142 insertions(+) create mode 100644 classes/privacy/provider.php diff --git a/.gitignore b/.gitignore index 233cd7d..c1c0029 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ *.code-workspace .idea/ + +.history diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php new file mode 100644 index 0000000..c782dfe --- /dev/null +++ b/classes/privacy/provider.php @@ -0,0 +1,129 @@ +. + +/** + * 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 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 '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('qtracker_issue', [ + 'userid' => 'privacy:metadata:qtracker_issue:userid', + 'title' => 'privacy:metadata:qtracker_issue:title', + 'description' => 'privacy:metadata:qtracker_issue:description', + 'timecreated' => 'privacy:metadata:qtracker_issue:timecreated' + ], 'privacy:metadata:qtracker_issue'); + + // The table '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('qtracker_comment', [ + 'userid' => 'privacy:metadata:qtracker_comment:userid', + 'description' => 'privacy:metadata:qtracker_comment:description', + 'timecreated' => 'privacy:metadata:qtracker_comment:timecreated' + ], 'privacy:metadata: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 qtracker_issue and qtracker_comment left join? on issueid (comments table) to get all contextids stored in the qtracker_issue table. + } + + /** + * 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) { + // TODO: Export all data from all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). + global $DB; + } + + /** + * 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) { + // TODO: for ALL USERS : delete all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). + global $DB; + } + + /** + * 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) { + // TODO: for one specific user, delete all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). + + global $DB; + + } +} diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php index 1e7e4b8..42e860d 100755 --- a/lang/en/local_qtracker.php +++ b/lang/en/local_qtracker.php @@ -96,3 +96,14 @@ $string['qtracker:editall'] = 'Edit all issues'; $string['qtracker:viewmine'] = 'Edit your own issues'; $string['qtracker:viewall'] = 'View all issues'; + +$string['privacy:metadata:qtracker_issue'] = 'Details about each question issue.'; +$string['privacy:metadata:qtracker_issue:userid'] = 'The user that created the issue.'; +$string['privacy:metadata:qtracker_issue:title'] = 'The title of the issue.'; +$string['privacy:metadata:qtracker_issue:description'] = 'The description of the issue.'; +$string['privacy:metadata:qtracker_issue:timecreated'] = 'The time the issue was created.'; + +$string['privacy:metadata:qtracker_comment'] = 'Details about each question issue comment.'; +$string['privacy:metadata:qtracker_issue:userid'] = 'The user that created the issue comment.'; +$string['privacy:metadata:qtracker_issue:description'] = 'The description of the issue comment.'; +$string['privacy:metadata:qtracker_issue:timecreated'] = 'The time the issue comment was created.'; From 12c5041c6894907c656e911c93ad90ced289bd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Storhaug?= Date: Mon, 14 Jun 2021 18:41:16 +0200 Subject: [PATCH 20/60] Finish privacy implementation --- classes/privacy/provider.php | 136 +++++++++++++++++++++++++++++++++-- lang/en/local_qtracker.php | 1 + version.php | 2 +- 3 files changed, 133 insertions(+), 6 deletions(-) diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index c782dfe..73dc6bc 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -90,6 +90,18 @@ public static function get_metadata(collection $items): collection { public static function get_contexts_for_userid(int $userid): contextlist { // TODO: select all from table qtracker_issue and qtracker_comment left join? on issueid (comments table) to get all contextids stored in the qtracker_issue table. + $sql = "SELECT qi.contextid + FROM {qtracker_issue} qi + LEFT JOIN {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; } /** @@ -101,8 +113,67 @@ public static function get_contexts_for_userid(int $userid): contextlist { * @throws moodle_exception */ public static function export_user_data(approved_contextlist $contextlist) { - // TODO: Export all data from all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). global $DB; + if (empty($contextlist)) { + return; + } + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $sql = "SELECT * FROM {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 {qtracker_issue} qi + INNER JOIN {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); + } } /** @@ -111,8 +182,32 @@ public static function export_user_data(approved_contextlist $contextlist) { * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(context $context) { - // TODO: for ALL USERS : delete all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). global $DB; + $sql = "SELECT qc.id AS id + FROM {qtracker_issue} qi + INNER JOIN {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('qtracker_comment', ['id' => $comment->id]); + } + + $sql = "SELECT id + FROM {qtracker_issue} + WHERE contextid = :contextid"; + $params = [ + 'contextid' => $context->id + ]; + $issues = $DB->get_records_sql($sql, $params); + + foreach ($issues as $issue) { + $DB->delete_records('qtracker_issue', ['id' => $issue->id]); + } } /** @@ -121,9 +216,40 @@ public static function delete_data_for_all_users_in_context(context $context) { * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { - // TODO: for one specific user, delete all issues with appropriate context id, but first delete all comments with correct contextid (linked in qtracker_issue table). - 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 {qtracker_issue} qi + INNER JOIN {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('qtracker_comment', ['id' => $comment->id]); + } + + $sql = "SELECT id + FROM {qtracker_issue} + WHERE qc.userid = :userid + AND qi.contextid {$contextsql}"; + $params = [ + 'userid' => $user->id + ]; + $params += $contextparams; + $issues = $DB->get_records_sql($sql, $params); + + foreach ($issues as $issue) { + $DB->delete_records('qtracker_issue', ['id' => $issue->id]); + } } } diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php index 42e860d..9b2da93 100755 --- a/lang/en/local_qtracker.php +++ b/lang/en/local_qtracker.php @@ -64,6 +64,7 @@ $string['issue'] = 'Issue'; $string['issues'] = 'Issues'; +$string['comments'] ='Comments'; $string['submitnewissue'] = 'Submit new issue'; $string['validtitle'] = 'Please provide a valid title.'; diff --git a/version.php b/version.php index a9884ed..f328f98 100755 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2020092200; +$plugin->version = 2021061400; $plugin->requires = 2016120500; $plugin->cron = 0; $plugin->component = 'local_qtracker'; From 0c689689ea349c337214fba8541281d507bd684b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Storhaug?= Date: Mon, 14 Jun 2021 18:42:31 +0200 Subject: [PATCH 21/60] Fix minor sql error --- classes/privacy/provider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 73dc6bc..4c6c51b 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -240,8 +240,8 @@ public static function delete_data_for_user(approved_contextlist $contextlist) { $sql = "SELECT id FROM {qtracker_issue} - WHERE qc.userid = :userid - AND qi.contextid {$contextsql}"; + WHERE userid = :userid + AND contextid {$contextsql}"; $params = [ 'userid' => $user->id ]; From 7a2cb20ca3eeef80d3ebc7fd82e12aa8274a5dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Storhaug?= Date: Mon, 14 Jun 2021 18:49:06 +0200 Subject: [PATCH 22/60] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c967865..2798710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ 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] +### Added +- Privacy API is implemented in order to comply with the [General Data Protection Regulation](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation) (GDPR). ## [0.1.0] - YYYY-MM-DD ### Fixed From c949caac7a34765bc9d1e6ad36a5e837e12302cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Storhaug?= Date: Mon, 14 Jun 2021 20:06:08 +0200 Subject: [PATCH 23/60] Update db table names --- classes/privacy/provider.php | 60 ++++++++++++++++++------------------ lang/en/local_qtracker.php | 20 ++++++------ 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 4c6c51b..b811b68 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -61,22 +61,22 @@ class provider implements * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $items): collection { - // The table 'qtracker_issue' stores a record for each qtracker issue. + // 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('qtracker_issue', [ - 'userid' => 'privacy:metadata:qtracker_issue:userid', - 'title' => 'privacy:metadata:qtracker_issue:title', - 'description' => 'privacy:metadata:qtracker_issue:description', - 'timecreated' => 'privacy:metadata:qtracker_issue:timecreated' - ], 'privacy:metadata:qtracker_issue'); - - // The table 'qtracker_comment' stores a record of each issue comment. + $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('qtracker_comment', [ - 'userid' => 'privacy:metadata:qtracker_comment:userid', - 'description' => 'privacy:metadata:qtracker_comment:description', - 'timecreated' => 'privacy:metadata:qtracker_comment:timecreated' - ], 'privacy:metadata:qtracker_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; } @@ -89,10 +89,10 @@ public static function get_metadata(collection $items): collection { */ public static function get_contexts_for_userid(int $userid): contextlist { - // TODO: select all from table qtracker_issue and qtracker_comment left join? on issueid (comments table) to get all contextids stored in the qtracker_issue table. + // 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 {qtracker_issue} qi - LEFT JOIN {qtracker_comment} qc + FROM {local_qtracker_issue} qi + LEFT JOIN {local_qtracker_comment} qc ON qi.id = qc.issueid WHERE qi.userid = :userid1 OR qc.userid = :userid2"; @@ -119,7 +119,7 @@ public static function export_user_data(approved_contextlist $contextlist) { } list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); - $sql = "SELECT * FROM {qtracker_issue} WHERE contextid $contextsql"; + $sql = "SELECT * FROM {local_qtracker_issue} WHERE contextid $contextsql"; $issues = $DB->get_records_sql($sql, $contextparams); $context = null; @@ -146,8 +146,8 @@ public static function export_user_data(approved_contextlist $contextlist) { qi.id AS issueid, qc.description AS description, qc.timecreated AS timecreated - FROM {qtracker_issue} qi - INNER JOIN {qtracker_comment} qc + FROM {local_qtracker_issue} qi + INNER JOIN {local_qtracker_comment} qc ON qi.id = qc.issueid WHERE qc.userid = :userid AND qi.contextid {$contextsql}"; @@ -184,8 +184,8 @@ public static function export_user_data(approved_contextlist $contextlist) { public static function delete_data_for_all_users_in_context(context $context) { global $DB; $sql = "SELECT qc.id AS id - FROM {qtracker_issue} qi - INNER JOIN {qtracker_comment} qc + FROM {local_qtracker_issue} qi + INNER JOIN {local_qtracker_comment} qc ON qc.issueid = qi.id WHERE qi.contextid = :contextid"; $params = [ @@ -194,11 +194,11 @@ public static function delete_data_for_all_users_in_context(context $context) { $comments = $DB->get_records_sql($sql, $params); foreach ($comments as $comment) { - $DB->delete_records('qtracker_comment', ['id' => $comment->id]); + $DB->delete_records('local_qtracker_comment', ['id' => $comment->id]); } $sql = "SELECT id - FROM {qtracker_issue} + FROM {local_qtracker_issue} WHERE contextid = :contextid"; $params = [ 'contextid' => $context->id @@ -206,7 +206,7 @@ public static function delete_data_for_all_users_in_context(context $context) { $issues = $DB->get_records_sql($sql, $params); foreach ($issues as $issue) { - $DB->delete_records('qtracker_issue', ['id' => $issue->id]); + $DB->delete_records('local_qtracker_issue', ['id' => $issue->id]); } } @@ -223,8 +223,8 @@ public static function delete_data_for_user(approved_contextlist $contextlist) { $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 {qtracker_issue} qi - INNER JOIN {qtracker_comment} qc + FROM {local_qtracker_issue} qi + INNER JOIN {local_qtracker_comment} qc ON qc.issueid = qi.id WHERE qc.userid = :userid AND qi.contextid {$contextsql}"; @@ -235,11 +235,11 @@ public static function delete_data_for_user(approved_contextlist $contextlist) { $comments = $DB->get_records_sql($sql, $params); foreach ($comments as $comment) { - $DB->delete_records('qtracker_comment', ['id' => $comment->id]); + $DB->delete_records('local_qtracker_comment', ['id' => $comment->id]); } $sql = "SELECT id - FROM {qtracker_issue} + FROM {local_qtracker_issue} WHERE userid = :userid AND contextid {$contextsql}"; $params = [ @@ -249,7 +249,7 @@ public static function delete_data_for_user(approved_contextlist $contextlist) { $issues = $DB->get_records_sql($sql, $params); foreach ($issues as $issue) { - $DB->delete_records('qtracker_issue', ['id' => $issue->id]); + $DB->delete_records('local_qtracker_issue', ['id' => $issue->id]); } } } diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php index 9b2da93..df8fd4d 100755 --- a/lang/en/local_qtracker.php +++ b/lang/en/local_qtracker.php @@ -98,13 +98,13 @@ $string['qtracker:viewmine'] = 'Edit your own issues'; $string['qtracker:viewall'] = 'View all issues'; -$string['privacy:metadata:qtracker_issue'] = 'Details about each question issue.'; -$string['privacy:metadata:qtracker_issue:userid'] = 'The user that created the issue.'; -$string['privacy:metadata:qtracker_issue:title'] = 'The title of the issue.'; -$string['privacy:metadata:qtracker_issue:description'] = 'The description of the issue.'; -$string['privacy:metadata:qtracker_issue:timecreated'] = 'The time the issue was created.'; - -$string['privacy:metadata:qtracker_comment'] = 'Details about each question issue comment.'; -$string['privacy:metadata:qtracker_issue:userid'] = 'The user that created the issue comment.'; -$string['privacy:metadata:qtracker_issue:description'] = 'The description of the issue comment.'; -$string['privacy:metadata:qtracker_issue:timecreated'] = 'The time the issue comment was created.'; +$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.'; From ec6450a829d528e2e110033a2cb05d85ec2e195d Mon Sep 17 00:00:00 2001 From: David Date: Sat, 26 Jun 2021 13:10:46 +0200 Subject: [PATCH 24/60] Some more restore progress --- .../backup_local_qtracker_plugin.class.php | 9 ++- .../restore_local_qtracker_plugin.class.php | 67 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/backup/moodle2/backup_local_qtracker_plugin.class.php b/backup/moodle2/backup_local_qtracker_plugin.class.php index 4f13943..14a8ea7 100644 --- a/backup/moodle2/backup_local_qtracker_plugin.class.php +++ b/backup/moodle2/backup_local_qtracker_plugin.class.php @@ -41,6 +41,8 @@ class backup_local_qtracker_plugin extends backup_local_plugin { protected function define_my_settings() { } + // TODO: test for quizqtracker + /** * Define qtracker structure from question entrypoint * @@ -59,14 +61,17 @@ protected function define_question_plugin_structure() { 'description', 'userid', 'timecreated' ]); + // TODO: Trenge ikkje questionusageid, slot, contextid + // Build the backup tree $qtrackerissue->add_child($qtrackercomment); $pluginwrapper->add_child($qtrackerissue); $plugin->add_child($pluginwrapper); // Define sources - $qtrackerissue->set_source_table('qtracker_issue', ['questionid' => backup::VAR_PARENTID]); - $qtrackercomment->set_source_table('qtracker_comment', ['issueid' => backup::VAR_PARENTID]); + // TODO: Source local_qtracker... + $qtrackerissue->set_source_table('local_qtracker_issue', ['questionid' => backup::VAR_PARENTID]); + $qtrackercomment->set_source_table('local_qtracker_comment', ['issueid' => backup::VAR_PARENTID]); // Define annotations // TODO: Make these work diff --git a/backup/moodle2/restore_local_qtracker_plugin.class.php b/backup/moodle2/restore_local_qtracker_plugin.class.php index c7f4d34..ad053ef 100644 --- a/backup/moodle2/restore_local_qtracker_plugin.class.php +++ b/backup/moodle2/restore_local_qtracker_plugin.class.php @@ -37,4 +37,71 @@ class restore_local_qtracker_plugin extends restore_local_plugin { + /** + * + */ + protected function define_question_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); + return $paths; // And we return the interesting paths. + } + + public function process_issue($data) { + global $DB; + + $data = (object)$data; + print_object($data); + + $this->get_task()-> + + $oldquestionid = $this->get_old_parentid('question'); + $oldcontextid = $this->get_old_parentid('context'); + + //$newquestionid = $this->get_new_parentid('question'); + //$questioncreated = (bool) $this->get_mappingid('question_created', $oldquestionid); + //$oldquestioncategoryid = $this->get_old_parentid('question_category'); + //$newquestioncategoryid = $this->get_new_parentid('question_category'); + //$questioncategory = $this->get_mapping('question_category', $newquestioncategoryid); + + $question = $this->get_mapping('question', $oldquestionid); + $context = $this->get_mapping('context', $oldcontextid); + $quba = $this->get_mapping('question_usage', $data->questionusageid); + + + echo '

Contextid

'; + print_object($this->get_mappingid('context',$this->get_task()->get_old_contextid())); + print_object($this->get_mapping('context',$this->get_task()->get_old_contextid())->id); + //$data->questionid = $newquestionid; + //echo "

Questioncategory

"; + //print_object($questioncategory); + //echo "

Question

"; + //print_object($question); + //echo "

Data

"; + //print_object($data); + //$DB->insert_record("local_qtracker_issue", $data); + + echo '

Question

'; + print_object($question); + echo '

Context

'; + print_object($context); + echo '

Quba

'; + print_object($quba); + echo '

'; + //print_object(); + echo '

'; + //print_object(); + + + //throw new Error("dsakd"); + $issue = \local_qtracker\issue::create($data->title, $data->description, $question, $this->get_mapping('context',$this->get_task()->get_old_contextid())->id, $quba); + + //throw new Error("sksk"); + + //$DB->insert_record('local_qtracker_issue', $data); + + } } From 576f9c4cc27025ddacad6597cbc2bd7dddcd3ddb Mon Sep 17 00:00:00 2001 From: David Date: Sun, 27 Jun 2021 17:48:46 +0200 Subject: [PATCH 25/60] aside block prototype --- classes/output/question_issue_page.php | 33 ++++++++++++ lang/en/local_qtracker.php | 1 + templates/aside_block.mustache | 71 ++++++++++++++++++++++++++ templates/question_issue_page.mustache | 12 ++--- 4 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 templates/aside_block.mustache diff --git a/classes/output/question_issue_page.php b/classes/output/question_issue_page.php index e31cbba..36fbc87 100644 --- a/classes/output/question_issue_page.php +++ b/classes/output/question_issue_page.php @@ -150,6 +150,39 @@ public function export_for_template(renderer_base $output) { $data->closebutton = $closebutton; } + // TODO replace sim-data with data from linked issues and tags implementations + $simtag = [ + "name"=> "tagname", + "text" => "Primary", + ]; + $simtag2 = [ + "name"=> "tagname", + "text" => "Primary", + ]; + $simtag3 = [ + "name"=> "tagname", + "text" => "Primary", + ]; + $simtag4 = [ + "name"=> "tagname", + "text" => "Primary", + ]; + $simlinkiss = [ + "name" => "Issue name", + "text" => "Issue text" + ]; + + $linkedissues = new stdClass(); + $linkedissues->label = get_string('linkedissues', 'local_qtracker'); + $linkedissues->items = [$simlinkiss]; + + $tags = new stdClass(); + $tags->label = get_string('tags', 'local_qtracker'); + $tags->items = [$simtag, $simtag2, $simtag3, $simtag4, ]; + + $asideblocks = [$linkedissues, $tags]; + $data->asideblocks = $asideblocks; + $commentbutton = new stdClass(); $commentbutton->primary = true; $commentbutton->name = "commentissue"; diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php index df8fd4d..e4d9c14 100755 --- a/lang/en/local_qtracker.php +++ b/lang/en/local_qtracker.php @@ -75,6 +75,7 @@ $string['name'] = 'Name'; $string['tags'] = 'Tags'; +$string['linkedissues'] = 'Linked issues'; $string['new'] = 'New'; $string['open'] = 'Open'; diff --git a/templates/aside_block.mustache b/templates/aside_block.mustache new file mode 100644 index 0000000..47092c2 --- /dev/null +++ b/templates/aside_block.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): + { + "label": "tags", + "items": [{text:"An action", state: "disabled"}, {text:"another action", state: "active"}, {text:"a third action", state: "active"} ], + } +}} + +
+
+
+
{{label}}
+ + + + + + + + + + +
+ + +
    + {{#items}} +
  • {{{text}}}
  • + {{/items}} + {{^items}} +
  • No {{label}}
  • + {{/items}} +
+
+
+ + diff --git a/templates/question_issue_page.mustache b/templates/question_issue_page.mustache index 8c9139f..96ea40f 100644 --- a/templates/question_issue_page.mustache +++ b/templates/question_issue_page.mustache @@ -112,12 +112,12 @@
-
- {{#str}} tags, local_qtracker {{/str}} -
-
- Tags here... -
+ + {{#asideblocks}} + {{> local_qtracker/aside_block}} + {{/asideblocks}} + +
From eb24be059eef374b926f2c2e5dcf4ce7577fd27e Mon Sep 17 00:00:00 2001 From: David Date: Mon, 28 Jun 2021 23:28:41 +0200 Subject: [PATCH 26/60] Added wrapper to comments --- backup/moodle2/backup_local_qtracker_plugin.class.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backup/moodle2/backup_local_qtracker_plugin.class.php b/backup/moodle2/backup_local_qtracker_plugin.class.php index 14a8ea7..4243356 100644 --- a/backup/moodle2/backup_local_qtracker_plugin.class.php +++ b/backup/moodle2/backup_local_qtracker_plugin.class.php @@ -57,6 +57,7 @@ protected function define_question_plugin_structure() { 'title', 'description', 'questionusageid', 'slot', 'state', 'userid', 'contextid', 'timecreated' ]); + $qtrackercommentwrapper = new backup_nested_element('comments'); $qtrackercomment = new backup_nested_element('comment', ['id'], [ 'description', 'userid', 'timecreated' ]); @@ -64,7 +65,8 @@ protected function define_question_plugin_structure() { // TODO: Trenge ikkje questionusageid, slot, contextid // Build the backup tree - $qtrackerissue->add_child($qtrackercomment); + $qtrackercommentwrapper->add_child($qtrackercomment); + $qtrackerissue->add_child($qtrackercommentwrapper); $pluginwrapper->add_child($qtrackerissue); $plugin->add_child($pluginwrapper); From d0c04e606d9187057b51204c87447ebae9012787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Storhaug?= Date: Wed, 7 Jul 2021 13:42:49 +0200 Subject: [PATCH 27/60] Main implementation of #26 --- amd/src/question_issue_page.js | 286 ++++++++++++++++++ ...tions_table.js => questions_table_page.js} | 53 +--- amd/src/sidebar.js | 214 +++++++++++++ classes/external/get_issue_children.php | 147 +++++++++ classes/external/get_issue_parents.php | 149 +++++++++ classes/external/get_issues.php | 2 +- classes/external/issue_exporter.php | 1 - classes/external/reference_exporter.php | 75 +++++ classes/issue.php | 72 ++++- classes/output/question_issue_page.php | 24 ++ classes/output/question_issues_page.php | 5 +- classes/output/questions_table.php | 2 +- classes/output/renderer.php | 2 +- classes/referable.php | 106 +++++++ classes/reference.php | 177 +++++++++++ classes/reference_manager.php | 116 +++++++ db/install.xml | 15 +- db/services.php | 20 ++ lang/en/local_qtracker.php | 3 + lib.php | 26 +- styles.css | 95 +++++- templates/issue_comment.mustache | 4 +- templates/issue_description.mustache | 9 +- templates/question_issue_page.mustache | 43 ++- ...tions.mustache => questions_page.mustache} | 6 +- ...{issues_pane.mustache => sidebar.mustache} | 23 +- ...m.mustache => sidebar_item_issue.mustache} | 6 +- version.php | 2 +- 28 files changed, 1596 insertions(+), 87 deletions(-) create mode 100644 amd/src/question_issue_page.js rename amd/src/{questions_table.js => questions_table_page.js} (79%) create mode 100644 amd/src/sidebar.js create mode 100644 classes/external/get_issue_children.php create mode 100644 classes/external/get_issue_parents.php create mode 100644 classes/external/reference_exporter.php create mode 100644 classes/referable.php create mode 100644 classes/reference.php create mode 100644 classes/reference_manager.php rename templates/{questions.mustache => questions_page.mustache} (85%) rename templates/{issues_pane.mustache => sidebar.mustache} (67%) rename templates/{issues_pane_item.mustache => sidebar_item_issue.mustache} (92%) diff --git a/amd/src/question_issue_page.js b/amd/src/question_issue_page.js new file mode 100644 index 0000000..c10eced --- /dev/null +++ b/amd/src/question_issue_page.js @@ -0,0 +1,286 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Manager for managing table of questions with issues. + * + * @module local_qtracker/QuestionsIssue + * @class QuestionsIssue + * @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 { get_string as getString } from 'core/str'; +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 QuestionIssuePage { + courseid = null; + questionid = null; + issueid = null; + parents = []; + filter = ['Open', 'New']; + + + constructor(courseid, questionid, issueid) { + this.courseid = courseid; + this.questionid = questionid; + this.issueid = issueid; + this.sidebar = new Sidebar('#question-issues-sidebar', true, "left", false, '30%', '1.25rem'); + this.init(); + this.initSidebar() + } + + async init() { + let parentsData = await this.loadIssueParents(this.issueid); + if (parentsData.parents.length > 0) { + this.parents = parentsData.parents; + console.log(this.parents) + + let supersededids = this.parents.map((parent) => { + return $('') + .attr("href", this._getIssueUrl(parent.id)) + .html("#"+ parent.id).prop('outerHTML'); + }).join(", "); + + console.log(supersededids) + this.notify({ + message: await getString('issuesuperseded', 'local_qtracker', supersededids), + announce: true, + type: "warning", + }); + } + } + + _getIssueUrl(issueid) { + let issueurl = url.relativeUrl('/local/qtracker/issue.php', { + courseid: this.courseid, + issueid: issueid, + }); + return issueurl; + } + + async initSidebar() { + await this.sidebar.render(); + let state = null; + + 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(); + + // 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); + 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); + }); + + window.openSidebarOptions = function () { + // TODO: add dropdown menu + console.log("Options in sidebar clicked"); + this.showClosed = true; + this.filter.push('Closed'); + this.applyFilter(); + }.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.indexOf($(this).find(".badge").text()) === -1) { + $(this).hide(); + } + }); + } + + resetFilter() { + this.sidebar.getItems().each(function () { + $(this).show(); + }); + } + + /** + * + * @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, + 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; + } + + async loadIssueParents(id) { + let userData; + userData = await Ajax.call([{ + methodname: 'local_qtracker_get_issue_parents', + args: { + issueid: id + } + }])[0]; + return userData; + } + + + + + notify(notification) { + 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) => { + $('#qtracker-notifications').html(html); + Templates.runTemplateJS(js); + }) + .catch((error) => { + console.error(error); + throw error; + }); + }; +} + +export default QuestionIssuePage; diff --git a/amd/src/questions_table.js b/amd/src/questions_table_page.js similarity index 79% rename from amd/src/questions_table.js rename to amd/src/questions_table_page.js index 0decb90..a86c901 100644 --- a/amd/src/questions_table.js +++ b/amd/src/questions_table_page.js @@ -27,6 +27,7 @@ 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 @@ -35,51 +36,34 @@ import url from 'core/url'; * * Each call to init gets it's own instance of this class. */ -class QuestionsTable { +class QuestionsTablePage { courseid = null; constructor(courseid) { this.courseid = courseid; - + this.sidebar = new Sidebar('#questions-table-sidebar', false, "right", false, '40%'); this.init(); } async init() { - var hidden = true; - - let context = { - close: { - "key": "fa-times", - "title": "Close", - "alt": "Close pane", - "extraclasses": "", - "unmappedIcon": false - } - }; - - await Templates.render('local_qtracker/issues_pane', context).then((html, js) => { - Templates.replaceNodeContents('#questions-table-sidebar', html, js); - }); + await this.sidebar.render(); window.showIssuesInPane = async function(id, state = null) { - $('.issues-pane-content .issues').empty(); - $('.issues-pane-content .loading').addClass("show"); + 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); - $('.issues-pane-title').html(link); + this.sidebar.setTitle(link); + this.sidebar.show(); // Get issues data. let issuesResponse = await this.loadIssues(id, state); let issues = issuesResponse.issues; - if (hidden) { - lol(); - } - // Get users data. let userids = [...new Set(issues.map(issue => issue.userid))]; let usersData = await this.loadUsersData(userids); @@ -91,11 +75,12 @@ class QuestionsTable { promises.push(this.addIssueItem(issueData, userData)); }); + self = this; // When all issue item promises are resolved. $.when.apply($, promises).done(function() { - $('.issues-pane-content .loading').removeClass("show"); + self.sidebar.setLoading(false); $.each(arguments, (index, argument) => { - Templates.appendNodeContents('.issues-pane-content .issues', argument.html, argument.js); + self.sidebar.addTemplateItem(argument.html, argument.js); }); }).catch(e => { console.error(e); @@ -103,17 +88,9 @@ class QuestionsTable { }.bind(this); - window.closeIssuesPane = function() { - if (!hidden) { - lol(); - } - }; + window.closeIssuesPane = function() {this.sidebar.hide()}.bind(this); + window.toggleIssuesPane = function() {this.sidebar.togglePane()}.bind(this); - window.lol = function togglePane() { - $('.qtracker-container').toggleClass('push-pane-over'); - $('#issues-pane').toggleClass("show"); - hidden = !hidden; - }; } /** @@ -146,7 +123,7 @@ class QuestionsTable { let state = issueData.state; paneContext[state] = true; - return Templates.render('local_qtracker/issues_pane_item', paneContext) + return Templates.render('local_qtracker/sidebar_item_issue', paneContext) .then(function(html, js) { return {html: html, js: js}; }); @@ -204,4 +181,4 @@ class QuestionsTable { } } -export default QuestionsTable; +export default QuestionsTablePage; diff --git a/amd/src/sidebar.js b/amd/src/sidebar.js new file mode 100644 index 0000000..88ce73b --- /dev/null +++ b/amd/src/sidebar.js @@ -0,0 +1,214 @@ +// 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/QuestionsIssue + * @class QuestionsIssue + * @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 Resizer from 'local_qtracker/resizer'; + +/** + * 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; + + constructor(container, show = false, side = 'right', loading = false, width = '40%', margin = '0px') { + this.container = container; // Container element + this.hidden = !show; + this.visible = show; + this.side = side; + this.loading = loading; + this.width = width; + this.margin = margin; + + 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 + ')'); + //document.body.style.backgroundColor = 'red'; + } 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'); + + //document.body.style.backgroundColor = 'blue'; + } + } + + async render() { + let context = { + close: { + "key": "fa-times", + "title": "Close", + "alt": "Close pane", + "extraclasses": "", + "unmappedIcon": false + }, + options: { + "key": "fa-cog", + "title": "Options", + "alt": "Show options", + "extraclasses": "", + "unmappedIcon": false + } + }; + //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.setWidth(this.width); + this.screenTest(this.mql) + /*this.resizer = new Resizer($('#qtracker-sidebar')[0], true, function(x,y) { + self.setWidth( + 'calc(' + x + 'px ' + ' - 30px)', + 'calc(' + x + 'px ' + ' - -' + self.margin + ')' + ) + });*/ + }); + } + + + 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'); + //$('#qtracker-sidebar').addClass('border-left'); + //$('#qtracker-sidebar').removeClass('border-right'); + } else if (side == 'left') { + $('#qtracker-sidebar').addClass('qtracker-sidebar-left'); + $('#qtracker-sidebar').removeClass('qtracker-sidebar-right'); + //$('#qtracker-sidebar').addClass('border-right'); + //$('#qtracker-sidebar').removeClass('border-left'); + } + } + + 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(); + } + + 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/classes/external/get_issue_children.php b/classes/external/get_issue_children.php new file mode 100644 index 0000000..aade44b --- /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 get_issue_children_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 get_issue_children($issueid) { + global $PAGE, $DB; + + $issue = array(); + $children = array(); + $warnings = array(); + + // Parameter validation. + $params = self::validate_parameters(self::get_issue_children_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->contextid); + self::validate_context($context); + + // Capability checking. + issue_require_capability_on($child, 'view'); + + $renderer = $PAGE->get_renderer('core'); + $exporter = new issue_exporter($child, ['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 get_issue_children_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..fd492e8 --- /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 get_issue_parents_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 get_issue_parents($issueid) { + global $PAGE, $DB; + + $issue = array(); + $parents = array(); + $warnings = array(); + + // Parameter validation. + $params = self::validate_parameters(self::get_issue_parents_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 get_issue_parents_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 index e7a7e15..6d82fce 100644 --- a/classes/external/get_issues.php +++ b/classes/external/get_issues.php @@ -66,7 +66,7 @@ public static function get_issues_parameters() { "id" (int) matching issue id, "questionid" (int) issue questionid, "state" (string) issue state, - "title" (Sstring) issue last name + "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') ) diff --git a/classes/external/issue_exporter.php b/classes/external/issue_exporter.php index a993166..f1f8a1b 100644 --- a/classes/external/issue_exporter.php +++ b/classes/external/issue_exporter.php @@ -28,7 +28,6 @@ defined('MOODLE_INTERNAL') || die(); use \core\external\exporter; -use \renderer_base; /** * Class for displaying a list of issue data. 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/issue.php b/classes/issue.php index 594ccfe..f673f8a 100644 --- a/classes/issue.php +++ b/classes/issue.php @@ -27,6 +27,10 @@ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot . '/local/qtracker/lib.php'); + +use local_qtracker\referable; + /** * Question issue class. * @@ -34,7 +38,7 @@ * @copyright 2020 André Storhaug * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class issue { +class issue extends referable { /** * @var \stdClass @@ -42,7 +46,7 @@ class issue { protected $issue = null; /** - * @var \stdClass + * @var array */ protected $comments = array(); @@ -171,13 +175,12 @@ public function get_issue_obj() { */ public function create_comment($description) { $comment = issue_comment::create($description, $this); - $comments = $this->get_comments(); - array_push($comments, $comment); + array_push($this->comments, $comment); return $comment; } /** - * Add a new commentto this issue. + * Add a new comment to this issue. * * @return \stdClass */ @@ -193,6 +196,65 @@ public function get_comments() { 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) { + $this->make_outgoing_reference($issue, LOCAL_QTRACKER_REFERENCE_SUPERSEDED); + } + + /** + * Supersede another issue. + * Read: "Issue passed as param is superseded by this issue." + * @param string $description + * + * @return \stdClass + */ + public function supersede_issue(issue $issue) { + $this->make_incoming_reference($issue, LOCAL_QTRACKER_REFERENCE_SUPERSEDED); + } + + /** + * Returns true if issue is superseded by any issue. + */ + public function is_superseded() { + $outref = $this->get_outgoing_references(); + $refs = reference_manager::filter_references_by_type($outref, 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 = []; + $outref = $this->get_outgoing_references(); + $refs = reference_manager::filter_references_by_type($outref, 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 has been subsumed under this issue) + */ + public function get_children() { + $children = []; + $outref = $this->get_incoming_references(); + $refs = reference_manager::filter_references_by_type($outref, LOCAL_QTRACKER_REFERENCE_SUPERSEDED); + foreach ($refs as $ref) { + array_push($children, issue::load($ref->get_source_id())); + } + return $children; + } + /** * Loads and returns issue with id $issueid * diff --git a/classes/output/question_issue_page.php b/classes/output/question_issue_page.php index e31cbba..be55d3f 100644 --- a/classes/output/question_issue_page.php +++ b/classes/output/question_issue_page.php @@ -39,7 +39,9 @@ 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. @@ -157,6 +159,27 @@ public function export_for_template(renderer_base $output) { $commentbutton->label = get_string('comment', 'local_qtracker'); $data->commentbutton = $commentbutton; + + $edittitlebutton = new stdClass(); + $edittitlebutton->primary = true; + $edittitlebutton->name = "edittitle"; + $edittitlebutton->classes = "p-r-1"; + $edittitlebutton->value = true; + $edittitlebutton->label = get_string('edit', 'local_qtracker'); + $data->edittitlebutton = $edittitlebutton; + $data->action = $PAGE->url; + + $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; + + $question = \question_bank::load_question($this->questionissue->get_questionid()); question_require_capability_on($question, 'use'); @@ -173,6 +196,7 @@ public function export_for_template(renderer_base $output) { $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); diff --git a/classes/output/question_issues_page.php b/classes/output/question_issues_page.php index 767dbf6..cab1907 100644 --- a/classes/output/question_issues_page.php +++ b/classes/output/question_issues_page.php @@ -54,8 +54,9 @@ class question_issues_page implements renderable, templatable { * * @param \local_qtracker\question_issues_table $questionissuestable */ - public function __construct(question_issues_table $questionissuestable) { + public function __construct(question_issues_table $questionissuestable, $courseid) { $this->questionissuestable = $questionissuestable; + $this->courseid = $courseid; } /** @@ -75,6 +76,8 @@ public function export_for_template(renderer_base $output) { $questionissues = ob_get_contents(); ob_end_clean(); $data->questionissues = $questionissues; + $data->courseid = $this->courseid; + return $data; } diff --git a/classes/output/questions_table.php b/classes/output/questions_table.php index 566c081..def4208 100644 --- a/classes/output/questions_table.php +++ b/classes/output/questions_table.php @@ -226,7 +226,7 @@ public function wrap_html_start() { return; } - // echo '
'; + // echo '
'; // echo '
'; // echo '
'; // echo '
'; diff --git a/classes/output/renderer.php b/classes/output/renderer.php index 7e1cf43..c30961b 100644 --- a/classes/output/renderer.php +++ b/classes/output/renderer.php @@ -60,7 +60,7 @@ public function render_question_issues_page(question_issues_page $page) { */ public function render_questions_page(questions_page $page) { $data = $page->export_for_template($this); - return parent::render_from_template('local_qtracker/questions', $data); + return parent::render_from_template('local_qtracker/questions_page', $data); } /** diff --git a/classes/referable.php b/classes/referable.php new file mode 100644 index 0000000..11d02b2 --- /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 \stdClass + */ + 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/install.xml b/db/install.xml index 66f9e08..9ab833a 100755 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -38,5 +38,18 @@ + + + + + + + + + + + + +
diff --git a/db/services.php b/db/services.php index 5a3352d..b633719 100644 --- a/db/services.php +++ b/db/services.php @@ -106,6 +106,24 @@ 'type' => 'read', 'ajax' => true, 'loginrequired' => true, + ), + 'local_qtracker_get_issue_parents' => array( + 'classname' => 'local_qtracker\external\get_issue_parents', + 'methodname' => 'get_issue_parents', + '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' => 'get_issue_children', + 'classpath' => '', + 'description' => 'Get issue children.', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => true, ) ); @@ -118,6 +136,8 @@ 'local_qtracker_delete_issue', 'local_qtracker_get_issue', 'local_qtracker_get_issues', + 'local_qtracker_get_issue_parents', + 'local_qtracker_get_issue_children', ), 'restrictedusers' => 0, 'enabled' => 1, diff --git a/lang/en/local_qtracker.php b/lang/en/local_qtracker.php index df8fd4d..b114457 100755 --- a/lang/en/local_qtracker.php +++ b/lang/en/local_qtracker.php @@ -62,6 +62,8 @@ $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'; @@ -79,6 +81,7 @@ $string['new'] = 'New'; $string['open'] = 'Open'; $string['closed'] = 'Closed'; +$string['newissue'] = 'New issue'; $string['reopenissue'] = 'Reopen issue'; $string['closeissue'] = 'Comment and close issue'; diff --git a/lib.php b/lib.php index 9d5ef39..758c8cc 100644 --- a/lib.php +++ b/lib.php @@ -27,6 +27,13 @@ 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 * @@ -71,7 +78,7 @@ function issue_has_capability_on($issueorid, $cap) { global $USER; if (is_numeric($issueorid)) { - $issue = issue::load((int)$issueorid); + $issue = issue::load((int)$issueorid)->get_issue_obj(); } else if (is_object($issueorid)) { if (isset($issueorid->contextid) && isset($issueorid->userid)) { $issue = $issueorid; @@ -112,3 +119,20 @@ function issue_require_capability_on($issue, $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; +} diff --git a/styles.css b/styles.css index b416b52..a80465d 100644 --- a/styles.css +++ b/styles.css @@ -1,16 +1,17 @@ -#issues-pane { +#qtracker-sidebar { width: 100%; z-index: 30; border: 1px solid #dee2e6; + background-color: #fff; } -.issues-pane-header { +.qtracker-sidebar-header { padding: .75rem; font-size: 1em; top: 0; } -.issues-pane-footer { +.qtracker-sidebar-footer { padding: .75rem; font-size: 1em; bottom: 0; @@ -20,6 +21,62 @@ 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; @@ -31,7 +88,7 @@ width: auto; } -.push-pane-over { +.qtracker-push-pane-over { padding-right: 0; } @@ -48,6 +105,17 @@ 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; @@ -58,20 +126,27 @@ } @media (min-width: 768px) { - #issues-pane { + #qtracker-sidebar { position: absolute; top: calc(-1.25rem - 2px); - right: calc(-1.25rem); height: calc(100% + 2 * 1.25rem + 2px); - border: unset; - border-left: 1px solid #dee2e6; 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); } - .push-pane-over { - padding-right: 40%; + .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; } } diff --git a/templates/issue_comment.mustache b/templates/issue_comment.mustache index 79a2013..97f9466 100644 --- a/templates/issue_comment.mustache +++ b/templates/issue_comment.mustache @@ -29,8 +29,8 @@
User picture -
-
+
+
{{fullname}} 
{{#str}} commentedon, local_qtracker {{/str}} diff --git a/templates/issue_description.mustache b/templates/issue_description.mustache index 745375c..ca8ba53 100644 --- a/templates/issue_description.mustache +++ b/templates/issue_description.mustache @@ -26,10 +26,11 @@ }}
User picture -
-
- {{fullname}}  -
+ +
+
+ {{fullname}}  +
{{#str}} openedissueon, local_qtracker {{/str}} {{#userdate}} {{timecreated}}, {{#str}} strftimedate, core_langconfig {{/str}} {{/userdate}}
diff --git a/templates/question_issue_page.mustache b/templates/question_issue_page.mustache index 8c9139f..aa13e22 100644 --- a/templates/question_issue_page.mustache +++ b/templates/question_issue_page.mustache @@ -30,8 +30,9 @@ "profileimageurl": "https://moodle.org/pix/u/f3.png" } }} -
+
+
{{#questionissue}}
@@ -40,13 +41,38 @@ {{title}} #{{id}}
-
-

- {{>local_qtracker/issue_state_badge}} -

+
+ {{#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 }} @@ -85,7 +111,7 @@
{{/comments}}
-
+
Profile image
@@ -125,3 +151,8 @@
{{/questionissue}}
+{{#js}} +require(['jquery', 'local_qtracker/question_issue_page'], function($, QuestionIssuePage) { + new QuestionIssuePage({{courseid}}, {{question.questionid}}, {{questionissue.id}}); +}); +{{/js}} diff --git a/templates/questions.mustache b/templates/questions_page.mustache similarity index 85% rename from templates/questions.mustache rename to templates/questions_page.mustache index bca14b7..a244277 100644 --- a/templates/questions.mustache +++ b/templates/questions_page.mustache @@ -24,7 +24,7 @@ "questions": "raw html" } }} -
+

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

@@ -34,7 +34,7 @@
{{#js}} -require(['jquery', 'local_qtracker/questions_table'], function($, QuestionsTable) { - new QuestionsTable({{courseid}}); +require(['jquery', 'local_qtracker/questions_table_page'], function($, QuestionsTablPage) { + new QuestionsTablPage({{courseid}}); }); {{/js}} diff --git a/templates/issues_pane.mustache b/templates/sidebar.mustache similarity index 67% rename from templates/issues_pane.mustache rename to templates/sidebar.mustache index 9945eef..03929f2 100644 --- a/templates/issues_pane.mustache +++ b/templates/sidebar.mustache @@ -15,35 +15,42 @@ along with Moodle. If not, see . }} {{! - @template local_qtracker/issues_pane + @template local_qtracker/sidebar - Issue pane template. + Sidebar template. Example context (json): { } }} -
+
-
+ -
+
{{>core/loading}}
-
+
-