From 666310decff7c4bc8cc3278dbe59a480f4988640 Mon Sep 17 00:00:00 2001 From: Tai Le Tan <55305972+tailetan@users.noreply.github.com> Date: Fri, 21 Feb 2025 03:17:05 +0700 Subject: [PATCH] Questionnaire author to sort free response on name and date/time in summary view (#596) Co-authored-by: tai.letan --- amd/build/table_sort.min.js | 10 +++ amd/build/table_sort.min.js.map | 1 + amd/src/table_sort.js | 78 +++++++++++++++++++++ classes/responsetype/text.php | 9 ++- styles.css | 16 +++++ templates/reportpage.mustache | 9 ++- templates/results_text.mustache | 62 +++++++++++++++-- tests/behat/table_sort.feature | 119 ++++++++++++++++++++++++++++++++ 8 files changed, 294 insertions(+), 10 deletions(-) create mode 100644 amd/build/table_sort.min.js create mode 100644 amd/build/table_sort.min.js.map create mode 100644 amd/src/table_sort.js create mode 100644 tests/behat/table_sort.feature diff --git a/amd/build/table_sort.min.js b/amd/build/table_sort.min.js new file mode 100644 index 00000000..3d2b599b --- /dev/null +++ b/amd/build/table_sort.min.js @@ -0,0 +1,10 @@ +/** + * JavaScript library for questionnaire response table sorting. + * + * @module mod_questionnaire/table_sort + * @copyright + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define("mod_questionnaire/table_sort",["jquery"],(function($){var t={init:function(){M.util.js_pending("mod_questionnaire_tablesort"),$(".qn-handcursor").on("click",t.sortcolumn),M.util.js_complete("mod_questionnaire_tablesort")},sortcolumn:function(e){e.preventDefault();var col=$(this).index(),id=$(this).closest("table").attr("id"),sortOrder=1;$(this).siblings().find('span[class^="icon-container-"]').hide(),$(this).siblings().removeClass("asc desc"),$(this).find('span[class^="icon-container-"]').removeAttr("style"),$(this).is(".asc")?($(this).removeClass("asc").addClass("desc"),sortOrder=-1):$(this).addClass("asc").removeClass("desc");var arrData=$(this).closest("table").find("tbody >tr:has(td.cell)").get();arrData.sort((function(a,b){var val1=$(a).children("td").eq(col).text(),val2=$(b).children("td").eq(col).text(),dateregx=/^\d{2}.*\d{4},/;return dateregx.test(val1)&&dateregx.test(val2)?(val1=new Date(val1))<(val2=new Date(val2))?-sortOrder:val1>val2?sortOrder:0:$.isNumeric(val1)&&$.isNumeric(val2)?1==sortOrder?val1-val2:val2-val1:val1val2?sortOrder:0})),$.each(arrData,(function(index,row){$("#"+id+" tbody").append(row)}))}};return t})); + +//# sourceMappingURL=table_sort.min.js.map \ No newline at end of file diff --git a/amd/build/table_sort.min.js.map b/amd/build/table_sort.min.js.map new file mode 100644 index 00000000..d2a7f022 --- /dev/null +++ b/amd/build/table_sort.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"table_sort.min.js","sources":["../src/table_sort.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * JavaScript library for questionnaire response table sorting.\n *\n * @module mod_questionnaire/table_sort\n * @copyright\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery'], function($) {\n\n var t = {\n /**\n * Initialise the event listeners.\n *\n */\n init: function() {\n M.util.js_pending('mod_questionnaire_tablesort');\n $(\".qn-handcursor\").on('click', t.sortcolumn);\n M.util.js_complete('mod_questionnaire_tablesort');\n },\n\n /**\n * Javascript for sorting s'Text Box' Response.\n * @param {Event} e\n */\n sortcolumn: function(e) {\n e.preventDefault();\n var col = $(this).index();\n var id = $(this).closest('table').attr('id');\n var sortOrder = 1;\n $(this).siblings().find('span[class^=\"icon-container-\"]').hide();\n $(this).siblings().removeClass('asc desc');\n $(this).find('span[class^=\"icon-container-\"]').removeAttr('style');\n if ($(this).is('.asc')) {\n $(this).removeClass('asc').addClass('desc');\n sortOrder = -1;\n } else {\n $(this).addClass('asc').removeClass('desc');\n }\n var arrData = $(this).closest('table').find('tbody >tr:has(td.cell)').get();\n arrData.sort(function (a, b) {\n var val1 = $(a).children('td').eq(col).text();\n var val2 = $(b).children('td').eq(col).text();\n // Regex to check for date sorting.\n var dateregx = /^\\d{2}.*\\d{4},/;\n if (dateregx.test(val1) && dateregx.test(val2)) {\n val1 = new Date(val1);\n val2 = new Date(val2);\n return (val1 < val2) ? -sortOrder : (val1 > val2) ? sortOrder : 0;\n } else if ($.isNumeric(val1) && $.isNumeric(val2)) {\n return sortOrder == 1 ? val1 - val2 : val2 - val1;\n } else {\n return (val1 < val2) ? -sortOrder : (val1 > val2) ? sortOrder : 0;\n }\n });\n /* Append the sorted rows to tbody*/\n $.each(arrData, function (index, row) {\n var tableid = $('#' + id + ' tbody');\n tableid.append(row);\n });\n },\n };\n return t;\n});"],"names":["define","$","t","init","M","util","js_pending","on","sortcolumn","js_complete","e","preventDefault","col","this","index","id","closest","attr","sortOrder","siblings","find","hide","removeClass","removeAttr","is","addClass","arrData","get","sort","a","b","val1","children","eq","text","val2","dateregx","test","Date","isNumeric","each","row","append"],"mappings":";;;;;;;AAsBAA,sCAAO,CAAC,WAAW,SAASC,OAEpBC,EAAI,CAKJC,KAAM,WACFC,EAAEC,KAAKC,WAAW,+BAClBL,EAAE,kBAAkBM,GAAG,QAASL,EAAEM,YAClCJ,EAAEC,KAAKI,YAAY,gCAOvBD,WAAY,SAASE,GACjBA,EAAEC,qBACEC,IAAMX,EAAEY,MAAMC,QACdC,GAAKd,EAAEY,MAAMG,QAAQ,SAASC,KAAK,MACnCC,UAAY,EAChBjB,EAAEY,MAAMM,WAAWC,KAAK,kCAAkCC,OAC1DpB,EAAEY,MAAMM,WAAWG,YAAY,YAC/BrB,EAAEY,MAAMO,KAAK,kCAAkCG,WAAW,SACtDtB,EAAEY,MAAMW,GAAG,SACXvB,EAAEY,MAAMS,YAAY,OAAOG,SAAS,QACpCP,WAAa,GAEbjB,EAAEY,MAAMY,SAAS,OAAOH,YAAY,YAEpCI,QAAUzB,EAAEY,MAAMG,QAAQ,SAASI,KAAK,0BAA0BO,MACtED,QAAQE,MAAK,SAAUC,EAAGC,OAClBC,KAAO9B,EAAE4B,GAAGG,SAAS,MAAMC,GAAGrB,KAAKsB,OACnCC,KAAOlC,EAAE6B,GAAGE,SAAS,MAAMC,GAAGrB,KAAKsB,OAEnCE,SAAW,wBACXA,SAASC,KAAKN,OAASK,SAASC,KAAKF,OACrCJ,KAAO,IAAIO,KAAKP,QAChBI,KAAO,IAAIG,KAAKH,QACQjB,UAAaa,KAAOI,KAAQjB,UAAY,EACzDjB,EAAEsC,UAAUR,OAAS9B,EAAEsC,UAAUJ,MACpB,GAAbjB,UAAiBa,KAAOI,KAAOA,KAAOJ,KAErCA,KAAOI,MAASjB,UAAaa,KAAOI,KAAQjB,UAAY,KAIxEjB,EAAEuC,KAAKd,SAAS,SAAUZ,MAAO2B,KACfxC,EAAE,IAAMc,GAAK,UACnB2B,OAAOD,iBAIpBvC"} \ No newline at end of file diff --git a/amd/src/table_sort.js b/amd/src/table_sort.js new file mode 100644 index 00000000..9d1ea8d0 --- /dev/null +++ b/amd/src/table_sort.js @@ -0,0 +1,78 @@ +// 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 . + +/** + * JavaScript library for questionnaire response table sorting. + * + * @module mod_questionnaire/table_sort + * @copyright + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery'], function($) { + + var t = { + /** + * Initialise the event listeners. + * + */ + init: function() { + M.util.js_pending('mod_questionnaire_tablesort'); + $(".qn-handcursor").on('click', t.sortcolumn); + M.util.js_complete('mod_questionnaire_tablesort'); + }, + + /** + * Javascript for sorting s'Text Box' Response. + * @param {Event} e + */ + sortcolumn: function(e) { + e.preventDefault(); + var col = $(this).index(); + var id = $(this).closest('table').attr('id'); + var sortOrder = 1; + $(this).siblings().find('span[class^="icon-container-"]').hide(); + $(this).siblings().removeClass('asc desc'); + $(this).find('span[class^="icon-container-"]').removeAttr('style'); + if ($(this).is('.asc')) { + $(this).removeClass('asc').addClass('desc'); + sortOrder = -1; + } else { + $(this).addClass('asc').removeClass('desc'); + } + var arrData = $(this).closest('table').find('tbody >tr:has(td.cell)').get(); + arrData.sort(function (a, b) { + var val1 = $(a).children('td').eq(col).text(); + var val2 = $(b).children('td').eq(col).text(); + // Regex to check for date sorting. + var dateregx = /^\d{2}.*\d{4},/; + if (dateregx.test(val1) && dateregx.test(val2)) { + val1 = new Date(val1); + val2 = new Date(val2); + return (val1 < val2) ? -sortOrder : (val1 > val2) ? sortOrder : 0; + } else if ($.isNumeric(val1) && $.isNumeric(val2)) { + return sortOrder == 1 ? val1 - val2 : val2 - val1; + } else { + return (val1 < val2) ? -sortOrder : (val1 > val2) ? sortOrder : 0; + } + }); + /* Append the sorted rows to tbody*/ + $.each(arrData, function (index, row) { + var tableid = $('#' + id + ' tbody'); + tableid.append(row); + }); + }, + }; + return t; +}); \ No newline at end of file diff --git a/classes/responsetype/text.php b/classes/responsetype/text.php index e10ba715..5b2a6752 100644 --- a/classes/responsetype/text.php +++ b/classes/responsetype/text.php @@ -116,7 +116,7 @@ public function get_results($rids=false, $anonymous=false) { 'WHERE question_id=' . $this->question->id . $rsql . ' AND t.response_id = r.id' . ' AND u.id = r.userid ' . - 'ORDER BY u.lastname, u.firstname, r.submitted'; + 'ORDER BY r.submitted DESC'; } return $DB->get_records_sql($sql, $params); } @@ -204,10 +204,15 @@ public function get_results_tags($weights, $participants, $respondents, $showtot } // The 'evencolor' attribute is used by the PDF template. $response->evencolor = $evencolor; + $response->date = userdate($row->submitted, get_string('strftimedatetime')); $pagetags->responses[] = (object)['response' => $response]; $evencolor = !$evencolor; } - + // sort table only when row count is greater than one. + if (count($weights) > 1) { + $pagetags->sortresponse = true; + } + $pagetags->tableid = $this->question->id; if ($showtotals == 1) { $pagetags->total = new \stdClass(); $pagetags->total->total = "($respondents)"; diff --git a/styles.css b/styles.css index 56974b77..6be6a001 100644 --- a/styles.css +++ b/styles.css @@ -442,7 +442,23 @@ td.selected { #page-mod-questionnaire-report .generaltable.questionnairereport td { border: 1px solid silver; } +#page-mod-questionnaire-report .generaltable[id] th.qn-handcursor { + cursor: pointer; +} + +#page-mod-questionnaire-report th.c0 a span[class^='icon-container-'], +#page-mod-questionnaire-report th.c1 a span.icon-container-asc, +#page-mod-questionnaire-report th.asc a span.icon-container-desc, +#page-mod-questionnaire-report th.desc a span.icon-container-asc { + display: none; +} + +#page-mod-questionnaire-report th.asc a span.icon-container-asc, +#page-mod-questionnaire-report th.desc a span.icon-container-desc { + display: inline-block; +} +#page-mod-questionnaire-report .frtlst, .qn-container .smalltext { font-size: 0.75em; } diff --git a/templates/reportpage.mustache b/templates/reportpage.mustache index e6c17b49..ad6c97bc 100644 --- a/templates/reportpage.mustache +++ b/templates/reportpage.mustache @@ -83,4 +83,11 @@ {{#responses}}{{{.}}}{{/responses}} {{#bottomnavigationbar}}
{{{.}}}
{{/bottomnavigationbar}} - \ No newline at end of file + +{{#js}} + require(['jquery', 'mod_questionnaire/table_sort'], function($, TableSort) { + $(document).ready(function() { + TableSort.init(); + }); + }); +{{/js}} \ No newline at end of file diff --git a/templates/results_text.mustache b/templates/results_text.mustache index 662e9052..6abc511b 100644 --- a/templates/results_text.mustache +++ b/templates/results_text.mustache @@ -50,11 +50,51 @@ }} {{#responses.0}} - +
- - + + {{/tableid}} + + @@ -63,21 +103,29 @@ {{#response}} - + {{#tableid}} + + {{/tableid}} + {{/response}} {{/responses}} +{{#responses.0}} + + +{{/responses.0}} {{#total}} - + - + {{/total}} {{#responses.0}} - +
{{#str}} respondent, mod_questionnaire {{/str}}{{#str}} response, mod_questionnaire {{/str}} + {{#sortresponse}} + + {{#str}} respondent, mod_questionnaire {{/str}} + + {{#pix}} t/sort_asc, moodle, {{#str}} sortbyx {{/str}}{{/pix}} + + + {{#pix}} t/sort_desc, moodle, {{#str}} sortbyxreverse {{/str}}{{/pix}} + + + {{/sortresponse}} + {{^sortresponse}} + {{#str}} respondent, mod_questionnaire {{/str}} + {{/sortresponse}} + {{#tableid}} +
{{#str}} firstname {{/str}}/{{#str}} lastname {{/str}}
+
+ {{#sortresponse}} + + {{#str}} date {{/str}} + + {{#pix}} t/sort_asc, moodle, {{#str}} sortbyx {{/str}}{{/pix}} + + + {{#pix}} t/sort_desc, moodle, {{#str}} sortbyxreverse {{/str}}{{/pix}} + + + {{/sortresponse}} + {{^sortresponse}} + {{#str}} date {{/str}} + {{/sortresponse}} + {{#str}} response, mod_questionnaire{{/str}}
{{{response.respondent}}}{{{response.text}}}{{{response.date}}}{{{response.text}}}
{{#str}} totalresponses, mod_questionnaire{{/str}} + {{#str}} totalresponses, mod_questionnaire{{/str}} {{total}}
{{/responses.0}} {{^responses}} diff --git a/tests/behat/table_sort.feature b/tests/behat/table_sort.feature new file mode 100644 index 00000000..8283bd14 --- /dev/null +++ b/tests/behat/table_sort.feature @@ -0,0 +1,119 @@ +@mod @mod_questionnaire +Feature: In questionnaire, Teacher should be able to sort Text box responses on date and username. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Wendi | Blake | student1@example.com | + | student2 | Jim | Lai | student2@example.com | + | student3 | Stephan | Parker | student3@example.com | + | student4 | Bobby | Anderson | student4@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + And the following "activities" exist: + | activity | name | description | course | idnumber | resume | navigate | progressbar | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 1 | 1 | 1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Questions" in current page administration + And I add a "Text Box" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Question Text | Enter testbox question | + And I add a "Numeric" question and I fill the form with: + | Question Name | Q2 | + | No | y | + | Question Text | Enter a number | + And I add a "Yes/No" question and I fill the form with: + | Question Name | Q3 | + | Yes | y | + | Question Text | Would you like to answer another question? | + And I log out + + @javascript + Scenario: No sorting for table with single row + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + When I navigate to "Answer the questions..." in current page administration + And I set the field "Enter testbox question" to "Test 1" + And I set the field "Enter a number" to "2" + And I click on "Yes" "radio" + And I press "Submit questionnaire" + And I press "Continue" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "View all responses" in current page administration + And "(//table[string-length(@id) > 1]//thead//th[2]//span[contains(@class,'icon-container-desc')])[1]" "xpath_element" should not exist + And "(//table[string-length(@id) > 1]//thead//th[2]//span[contains(@class,'icon-container-asc')])[1]" "xpath_element" should not exist + + @javascript + Scenario: Sort the table with multiple row + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + When I navigate to "Answer the questions..." in current page administration + And I set the field "Enter testbox question" to "Test 1" + And I set the field "Enter a number" to "2" + And I click on "Yes" "radio" + And I press "Submit questionnaire" + And I press "Continue" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + When I navigate to "Answer the questions..." in current page administration + And I set the field "Enter testbox question" to "StuTest 2" + And I set the field "Enter a number" to "1" + And I click on "Yes" "radio" + And I press "Submit questionnaire" + And I press "Continue" + And I log out + And I log in as "student3" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + When I navigate to "Answer the questions..." in current page administration + And I set the field "Enter testbox question" to "Test 3" + And I set the field "Enter a number" to "3" + And I click on "Yes" "radio" + And I press "Submit questionnaire" + And I press "Continue" + And I log out + And I log in as "student4" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + When I navigate to "Answer the questions..." in current page administration + And I set the field "Enter testbox question" to "Test 4" + And I set the field "Enter a number" to "4" + And I click on "Yes" "radio" + And I press "Submit questionnaire" + And I press "Continue" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "View all responses" in current page administration + And I click on "(//table[string-length(@id) > 1]//thead//th[contains(@class, 'c0 qn-handcursor')])[1]" "xpath_element" + And "(//table[string-length(@id) > 1]//thead//th[contains(@class, 'asc')])[1]" "xpath_element" should exist + And I should see "Bobby Anderson" in the "(//table[string-length(@id) > 1]//tbody//tr[1]//td[1])[1]" "xpath_element" + And I should see "Jim Lai" in the "(//table[string-length(@id) > 1]//tbody//tr[2]//td[1])[1]" "xpath_element" + And I should see "Stephan Parker" in the "(//table[string-length(@id) > 1]//tbody//tr[3]//td[1])[1]" "xpath_element" + And I should see "Wendi Blake" in the "(//table[string-length(@id) > 1]//tbody//tr[4]//td[1])[1]" "xpath_element" + And I click on "(//table[string-length(@id) > 1]//thead//th[contains(@class, 'c0 qn-handcursor')])[1]" "xpath_element" + And "(//table[string-length(@id) > 1]//thead//th[contains(@class, 'desc')])[1]" "xpath_element" should exist + And I should see "Wendi Blake" in the "(//table[string-length(@id) > 1]//tbody//tr[1]//td[1])[1]" "xpath_element" + And I should see "Stephan Parker" in the "(//table[string-length(@id) > 1]//tbody//tr[2]//td[1])[1]" "xpath_element" + And I should see "Jim Lai" in the "(//table[string-length(@id) > 1]//tbody//tr[3]//td[1])[1]" "xpath_element" + And I should see "Bobby Anderson" in the "(//table[string-length(@id) > 1]//tbody//tr[4]//td[1])[1]" "xpath_element"