From 064f15033f8d92c64f7029afc1a2da53534288d5 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 25 Jul 2018 17:12:10 +0800 Subject: [PATCH] MDL-60474 assign: Consistent user filters Use the current filters and sorting on the user grading table in the single page grading app when it is possible. This replaces the popover used to configure the filters to one that closely matches the one from the grading table. It supports standard filters, workflow filters and allocated marker filters. It will also support group filtering and suspended user filtering but we don't show the controls for those in the single grading page. --- .../amd/build/grading_navigation.min.js | 2 +- .../build/grading_navigation_user_info.min.js | 2 +- .../amd/build/participant_selector.min.js | 2 +- mod/assign/amd/src/grading_navigation.js | 201 +++++++++++------ .../amd/src/grading_navigation_user_info.js | 24 +- mod/assign/amd/src/participant_selector.js | 9 +- mod/assign/classes/output/grading_app.php | 12 +- mod/assign/externallib.php | 19 +- mod/assign/lib.php | 26 +++ mod/assign/locallib.php | 213 +++++++++++++++--- mod/assign/styles.css | 10 +- .../grading_navigation_user_selector.mustache | 86 +++++-- mod/assign/tests/behat/grading_status.feature | 18 ++ mod/assign/tests/externallib_test.php | 4 +- mod/assign/tests/locallib_test.php | 13 ++ .../grading_navigation_user_selector.mustache | 42 +++- 16 files changed, 525 insertions(+), 158 deletions(-) diff --git a/mod/assign/amd/build/grading_navigation.min.js b/mod/assign/amd/build/grading_navigation.min.js index bc111f02502..a2abc281fa1 100644 --- a/mod/assign/amd/build/grading_navigation.min.js +++ b/mod/assign/amd/build/grading_navigation.min.js @@ -1 +1 @@ -define(["jquery","core/notification","core/str","core/form-autocomplete","core/ajax","mod_assign/grading_form_change_checker"],function(a,b,c,d,e,f){var g=function(e){this._regionSelector=e,this._region=a(e),this._filters=[],this._users=[],this._filteredUsers=[],this._loadAllUsers(),this._region.find('[data-action="previous-user"]').on("click",this._handlePreviousUser.bind(this)),this._region.find('[data-action="next-user"]').on("click",this._handleNextUser.bind(this)),this._region.find('[data-action="change-user"]').on("change",this._handleChangeUser.bind(this)),this._region.find('[data-region="user-filters"]').on("click",this._toggleExpandFilters.bind(this)),a(document).on("user-changed",this._refreshSelector.bind(this)),a(document).on("done-saving-show-next",this._handleNextUser.bind(this));var f=this._region.find('[data-region="user-filters"]'),g=a(document.getElementById(f.attr("aria-controls")));g.on("change",'[type="checkbox"]',this._filterChanged.bind(this));var h=a('[data-region="grading-navigation-panel"]').data("first-userid");h&&this._selectUserById(h),c.get_string("changeuser","mod_assign").done(function(a){d.enhance("[data-action=change-user]",!1,"mod_assign/participant_selector",a)}).fail(b.exception),a(document).bind("start-loading-user",function(){this._isLoading=!0}.bind(this)),a(document).bind("finish-loading-user",function(){this._isLoading=!1}.bind(this))};return g.prototype._isLoading=!1,g.prototype._regionSelector=null,g.prototype._filters=null,g.prototype._users=null,g.prototype._region=null,g.prototype._loadAllUsers=function(){var a=this._region.find("[data-action=change-user]"),c=a.attr("data-assignmentid"),d=a.attr("data-groupid");e.call([{methodname:"mod_assign_list_participants",args:{assignid:c,groupid:d,filter:"",onlyids:!0},done:this._usersLoaded.bind(this),fail:b.exception}])},g.prototype._usersLoaded=function(b){if(this._filteredUsers=this._users=b,this._users.length){var c=this._region.find('[data-region="user-filters"]'),d=a(document.getElementById(c.attr("aria-controls")));d.find('[type="checkbox"]').trigger("change")}else this._selectNoUser();this._triggerNextUserEvent()},g.prototype._checkClickOutsideConfigureFilters=function(b){var c=this._region.find('[data-region="configure-filters"]');if(!c.is(b.target)&&0===c.has(b.target).length){var d=this._region.find('[data-region="user-filters"]');c.hide(),c.attr("aria-hidden","true"),d.attr("aria-expanded","false"),a(document).unbind("click.mod_assign_grading_navigation")}},g.prototype._filterChanged=function(d){var e=a(d.target).attr("name"),f=e.split("_").pop(),g=a(d.target).prop("checked");if(g)this._filters.indexOf(f)==-1&&(this._filters[this._filters.length]=f);else{var h=this._filters.indexOf(f);h!=-1&&this._filters.splice(h,1)}var i=[];this._region.find('[data-region="configure-filters"]').find('[type="checkbox"]').each(function(b,c){a(c).prop("checked")&&(i[i.length]=a(c).closest("label").text())}),i.length?this._region.find('[data-region="user-filters"] span').text(i.join(", ")):c.get_string("nofilters","mod_assign").done(function(a){this._region.find('[data-region="user-filters"] span').text(a)}.bind(this)).fail(b.exception);var j=this._region.find("[data-action=change-user]"),k=j.attr("data-selected"),l=0;this._filteredUsers=[],a.each(this._users,function(b,c){var d=!0;a.each(this._filters,function(a,b){"submitted"==b?"0"==c.submitted&&(d=!1):"notsubmitted"==b?"1"==c.submitted&&(d=!1):"requiregrading"==b?"0"==c.requiregrading&&(d=!1):"grantedextension"==b&&"0"==c.grantedextension&&(d=!1)}),d&&(this._filteredUsers[this._filteredUsers.length]=c,k==c.id&&(l=this._filteredUsers.length-1))}.bind(this)),this._filteredUsers.length?this._selectUserById(this._filteredUsers[l].id):this._selectNoUser(),this._triggerNextUserEvent()},g.prototype._selectNoUser=function(){this._isLoading||(f.checkFormForChanges('[data-region="grade-panel"] .gradeform')?c.get_strings([{key:"unsavedchanges",component:"mod_assign"},{key:"unsavedchangesquestion",component:"mod_assign"},{key:"saveandcontinue",component:"mod_assign"},{key:"cancel",component:"core"}]).done(function(c){b.confirm(c[0],c[1],c[2],c[3],function(){a(document).trigger("save-changes",-1)})}):a(document).trigger("user-changed",-1))},g.prototype._selectUserById=function(d){var e=this._region.find("[data-action=change-user]"),g=parseInt(d,10);this._isLoading||(f.checkFormForChanges('[data-region="grade-panel"] .gradeform')?c.get_strings([{key:"unsavedchanges",component:"mod_assign"},{key:"unsavedchangesquestion",component:"mod_assign"},{key:"saveandcontinue",component:"mod_assign"},{key:"cancel",component:"core"}]).done(function(c){b.confirm(c[0],c[1],c[2],c[3],function(){a(document).trigger("save-changes",g)})}):(e.attr("data-selected",d),!isNaN(g)&&g>0&&a(document).trigger("user-changed",d)))},g.prototype._toggleExpandFilters=function(b){b.preventDefault();var c=a(b.target).closest('[data-region="user-filters"]'),d="true"==c.attr("aria-expanded"),e=a(document.getElementById(c.attr("aria-controls")));d?(e.hide(),e.attr("aria-hidden","true"),c.attr("aria-expanded","false"),a(document).unbind("click.mod_assign_grading_navigation")):(e.css("display","inline-block"),e.attr("aria-hidden","false"),c.attr("aria-expanded","true"),b.stopPropagation(),a(document).on("click.mod_assign_grading_navigation",this._checkClickOutsideConfigureFilters.bind(this)))},g.prototype._handlePreviousUser=function(a){a.preventDefault();var b=this._region.find("[data-action=change-user]"),c=b.attr("data-selected"),d=0,e=0;for(d=0;d0&&a(document).trigger("user-changed",j)}else h&&this._selectUserById(this._filteredUsers[i].id)},g.prototype._refreshCount=function(){var a=this._region.find("[data-action=change-user]"),d=a.attr("data-selected"),e=0,f=0;if(isNaN(d)||d<=0)this._region.find('[data-region="user-count"]').hide();else{for(this._region.find('[data-region="user-count"]').show(),e=0;e0&&c.attr("data-selected",b),this._refreshCount()},g.prototype._triggerNextUserEvent=function(){this._filteredUsers.length>1?a(document).trigger("next-user",{nextUserId:null,nextUser:!0}):a(document).trigger("next-user",{nextUser:!1})},g.prototype._handleChangeUser=function(){var d=this._region.find("[data-action=change-user]"),e=parseInt(d.val(),10);this._isLoading||(f.checkFormForChanges('[data-region="grade-panel"] .gradeform')?c.get_strings([{key:"unsavedchanges",component:"mod_assign"},{key:"unsavedchangesquestion",component:"mod_assign"},{key:"saveandcontinue",component:"mod_assign"},{key:"cancel",component:"core"}]).done(function(c){b.confirm(c[0],c[1],c[2],c[3],function(){a(document).trigger("save-changes",e)})}):!isNaN(e)&&e>0&&(d.attr("data-selected",e),a(document).trigger("user-changed",e)))},g}); \ No newline at end of file +define(["jquery","core/notification","core/str","core/form-autocomplete","core/ajax","mod_assign/grading_form_change_checker"],function(a,b,c,d,e,f){var g=function(e){this._regionSelector=e,this._region=a(e),this._filters=[],this._users=[],this._filteredUsers=[],this._lastXofYUpdate=0,this._firstLoadUsers=!0,this._loadAllUsers(),this._region.find('[data-action="previous-user"]').on("click",this._handlePreviousUser.bind(this)),this._region.find('[data-action="next-user"]').on("click",this._handleNextUser.bind(this)),this._region.find('[data-action="change-user"]').on("change",this._handleChangeUser.bind(this)),this._region.find('[data-region="user-filters"]').on("click",this._toggleExpandFilters.bind(this)),a(document).on("user-changed",this._refreshSelector.bind(this)),a(document).on("done-saving-show-next",this._handleNextUser.bind(this));var f=this._region.find('[data-region="user-filters"]'),g=a(document.getElementById(f.attr("aria-controls")));g.on("change","select",this._filterChanged.bind(this));var h=a('[data-region="grading-navigation-panel"]').data("first-userid");h&&this._selectUserById(h),c.get_string("changeuser","mod_assign").done(function(a){d.enhance("[data-action=change-user]",!1,"mod_assign/participant_selector",a)}).fail(b.exception),a(document).bind("start-loading-user",function(){this._isLoading=!0}.bind(this)),a(document).bind("finish-loading-user",function(){this._isLoading=!1}.bind(this))};return g.prototype._isLoading=!1,g.prototype._regionSelector=null,g.prototype._filters=null,g.prototype._users=null,g.prototype._region=null,g.prototype._lastFilters="",g.prototype._loadAllUsers=function(){var a=this._region.find("[data-action=change-user]"),c=a.attr("data-assignmentid"),d=a.attr("data-groupid"),f=this._region.find('[data-region="configure-filters"]'),g=f.find('select[name="filter"]').val(),h=f.find('select[name="workflowfilter"]');h&&(g+=","+h.val());var i=f.find('select[name="markerfilter"]');return i&&(g+=","+i.val()),this._lastFilters!=g&&(this._lastFilters=g,e.call([{methodname:"mod_assign_list_participants",args:{assignid:c,groupid:d,filter:"",onlyids:!0,tablesort:!0},done:this._usersLoaded.bind(this),fail:b.exception}]),!0)},g.prototype._usersLoaded=function(b){if(this._firstLoadUsers=!1,this._filteredUsers=this._users=b,this._users.length){var c=this._region.find('[data-region="user-filters"]'),d=a(document.getElementById(c.attr("aria-controls")));d.find('select[name="filter"]').trigger("change")}else this._selectNoUser();this._triggerNextUserEvent()},g.prototype._checkClickOutsideConfigureFilters=function(b){var c=this._region.find('[data-region="configure-filters"]');if(!c.is(b.target)&&0===c.has(b.target).length){var d=this._region.find('[data-region="user-filters"]');c.hide(),c.attr("aria-hidden","true"),d.attr("aria-expanded","false"),a(document).unbind("click.mod_assign_grading_navigation")}},g.prototype._updateFilterPreferences=function(b,c,d){var f=[],g=0;if(0==c.length||this._firstLoadUsers){var h=a.Deferred();return h.resolve(),h}for(g=0;g0&&a(document).trigger("user-changed",d)))},g.prototype._toggleExpandFilters=function(b){b.preventDefault();var c=a(b.target).closest('[data-region="user-filters"]'),d="true"==c.attr("aria-expanded"),e=a(document.getElementById(c.attr("aria-controls")));d?(e.hide(),e.attr("aria-hidden","true"),c.attr("aria-expanded","false"),a(document).unbind("click.mod_assign_grading_navigation")):(e.css("display","inline-block"),e.attr("aria-hidden","false"),c.attr("aria-expanded","true"),b.stopPropagation(),a(document).on("click.mod_assign_grading_navigation",this._checkClickOutsideConfigureFilters.bind(this)))},g.prototype._handlePreviousUser=function(a){a.preventDefault();var b=this._region.find("[data-action=change-user]"),c=b.attr("data-selected"),d=0,e=0;for(d=0;d0&&a(document).trigger("user-changed",j)}else h&&this._selectUserById(this._filteredUsers[i].id)},g.prototype._setCountString=function(a,d){var e=0;this._lastXofYUpdate++,e=this._lastXofYUpdate;var f={x:a,y:d};c.get_string("xofy","mod_assign",f).done(function(a){e==this._lastXofYUpdate&&this._region.find('[data-region="user-count-summary"]').text(a)}.bind(this)).fail(b.exception)},g.prototype._refreshCount=function(){var a=this._region.find("[data-action=change-user]"),b=a.attr("data-selected"),c=0,d=0;if(isNaN(b)||b<=0)this._region.find('[data-region="user-count"]').hide();else{for(this._region.find('[data-region="user-count"]').show(),c=0;c0){var f=new URL(window.location);if(parseInt(f.searchParams.get("blindid"))>0){var g=this._filteredUsers[d-1].recordid;f.searchParams.set("blindid",g)}else f.searchParams.set("userid",b);window.history.replaceState({},"",f)}}},g.prototype._refreshSelector=function(a,b){var c=this._region.find("[data-action=change-user]");b=parseInt(b,10),!isNaN(b)&&b>0&&c.attr("data-selected",b),this._refreshCount()},g.prototype._triggerNextUserEvent=function(){this._filteredUsers.length>1?a(document).trigger("next-user",{nextUserId:null,nextUser:!0}):a(document).trigger("next-user",{nextUser:!1})},g.prototype._handleChangeUser=function(){var d=this._region.find("[data-action=change-user]"),e=parseInt(d.val(),10);this._isLoading||(f.checkFormForChanges('[data-region="grade-panel"] .gradeform')?c.get_strings([{key:"unsavedchanges",component:"mod_assign"},{key:"unsavedchangesquestion",component:"mod_assign"},{key:"saveandcontinue",component:"mod_assign"},{key:"cancel",component:"core"}]).done(function(c){b.confirm(c[0],c[1],c[2],c[3],function(){a(document).trigger("save-changes",e)})}):!isNaN(e)&&e>0&&(d.attr("data-selected",e),a(document).trigger("user-changed",e)))},g}); \ No newline at end of file diff --git a/mod/assign/amd/build/grading_navigation_user_info.min.js b/mod/assign/amd/build/grading_navigation_user_info.min.js index d643460b978..b80849665af 100644 --- a/mod/assign/amd/build/grading_navigation_user_info.min.js +++ b/mod/assign/amd/build/grading_navigation_user_info.min.js @@ -1 +1 @@ -define(["jquery","core/notification","core/ajax","core/templates"],function(a,b,c,d){var e=function(b){this._regionSelector=b,this._region=a(b),this._userCache={},a(document).on("user-changed",this._refreshUserInfo.bind(this))};return e.prototype._regionSelector=null,e.prototype._userCache=null,e.prototype._region=null,e.prototype._lastUserId=0,e.prototype._getAssignmentId=function(){return this._region.attr("data-assignmentid")},e.prototype._refreshUserInfo=function(e,f){var g=a.Deferred();this._lastUserId!=f&&(this._lastUserId=f,d.render("mod_assign/loading",{}).done(function(e,h){if(this._region.fadeOut("fast",function(){d.replaceNodeContents(this._region,e,h),this._region.fadeIn("fast")}.bind(this)),f<0)return void d.render("mod_assign/grading_navigation_no_users",{}).done(function(a,b){this._region.fadeOut("fast",function(){d.replaceNodeContents(this._region,a,b),this._region.fadeIn("fast")}.bind(this))}.bind(this)).fail(b.exception);if("undefined"!=typeof this._userCache[f])g.resolve(this._userCache[f]);else{var i=this._getAssignmentId(),j=c.call([{methodname:"mod_assign_get_participant",args:{userid:f,assignid:i,embeduser:!0}}]);j[0].done(function(a){a.hasOwnProperty("id")?(this._userCache[f]=a,g.resolve(this._userCache[f])):g.reject("No users")}.bind(this)).fail(b.exception)}g.done(function(c){var e=a("[data-showuseridentity]").data("showuseridentity").split(","),f=[];c.courseid=a('[data-region="grading-navigation-panel"]').attr("data-courseid"),c.user&&(a.each(e,function(a,b){"undefined"!=typeof c.user[b]&&""!==c.user[b]&&(c.hasidentity=!0,f.push(c.user[b]))}),c.identity=f.join(", "),c.user.profileimageurl&&(c.profileimageurl=c.user.profileimageurl)),d.render("mod_assign/grading_navigation_user_summary",c).done(function(a,b){this._region.fadeOut("fast",function(){d.replaceNodeContents(this._region,a,b),this._region.fadeIn("fast")}.bind(this))}.bind(this)).fail(b.exception)}.bind(this)).fail(function(){d.render("mod_assign/grading_navigation_no_users",{}).done(function(a,b){this._region.fadeOut("fast",function(){d.replaceNodeContents(this._region,a,b),this._region.fadeIn("fast")}.bind(this))}.bind(this)).fail(b.exception)}.bind(this))}.bind(this)).fail(b.exception))},e}); \ No newline at end of file +define(["jquery","core/notification","core/ajax","core/templates"],function(a,b,c,d){var e=function(b){this._regionSelector=b,this._region=a(b),this._userCache={},a(document).on("user-changed",this._refreshUserInfo.bind(this))};return e.prototype._regionSelector=null,e.prototype._userCache=null,e.prototype._region=null,e.prototype._lastUserId=0,e.prototype._getAssignmentId=function(){return this._region.attr("data-assignmentid")},e.prototype._refreshUserInfo=function(e,f){var g=a.Deferred();this._lastUserId!=f&&(this._lastUserId=f,d.render("mod_assign/loading",{}).done(function(e,h){if(this._region.fadeOut("fast",function(){d.replaceNodeContents(this._region,e,h),this._region.fadeIn("fast")}.bind(this)),f<0)return void d.render("mod_assign/grading_navigation_no_users",{}).done(function(a,b){f==this._lastUserId&&this._region.fadeOut("fast",function(){d.replaceNodeContents(this._region,a,b),this._region.fadeIn("fast")}.bind(this))}.bind(this)).fail(b.exception);if("undefined"!=typeof this._userCache[f])g.resolve(this._userCache[f]);else{var i=this._getAssignmentId(),j=c.call([{methodname:"mod_assign_get_participant",args:{userid:f,assignid:i,embeduser:!0}}]);j[0].done(function(a){a.hasOwnProperty("id")?(this._userCache[f]=a,g.resolve(this._userCache[f])):g.reject("No users")}.bind(this)).fail(b.exception)}g.done(function(c){var e=a("[data-showuseridentity]").data("showuseridentity").split(","),g=[];c.courseid=a('[data-region="grading-navigation-panel"]').attr("data-courseid"),c.user&&(a.each(e,function(a,b){"undefined"!=typeof c.user[b]&&""!==c.user[b]&&(c.hasidentity=!0,g.push(c.user[b]))}),c.identity=g.join(", "),c.user.profileimageurl&&(c.profileimageurl=c.user.profileimageurl)),d.render("mod_assign/grading_navigation_user_summary",c).done(function(a,b){f==this._lastUserId&&this._region.fadeOut("fast",function(){d.replaceNodeContents(this._region,a,b),this._region.fadeIn("fast")}.bind(this))}.bind(this)).fail(b.exception)}.bind(this)).fail(function(){d.render("mod_assign/grading_navigation_no_users",{}).done(function(a,b){this._region.fadeOut("fast",function(){d.replaceNodeContents(this._region,a,b),this._region.fadeIn("fast")}.bind(this))}.bind(this)).fail(b.exception)}.bind(this))}.bind(this)).fail(b.exception))},e}); \ No newline at end of file diff --git a/mod/assign/amd/build/participant_selector.min.js b/mod/assign/amd/build/participant_selector.min.js index 7ad166abea3..072b89d417c 100644 --- a/mod/assign/amd/build/participant_selector.min.js +++ b/mod/assign/amd/build/participant_selector.min.js @@ -1 +1 @@ -define(["core/ajax","jquery","core/templates"],function(a,b,c){return{processResults:function(a,b){return b},transport:function(d,e,f,g){var h=b(d).attr("data-assignmentid"),i=b(d).attr("data-groupid"),j=b('[data-region="configure-filters"] input[type="checkbox"]'),k=[];j.each(function(a,c){k[b(c).attr("name")]=b(c).prop("checked")}),a.call([{methodname:"mod_assign_list_participants",args:{assignid:h,groupid:i,filter:e,limit:30,includeenrolments:!1}}])[0].then(function(a){var d=[],e=b("[data-showuseridentity]").data("showuseridentity").split(",");return b.each(a,function(a,f){var g=f,h=[],i=!0;k.filter_submitted&&!f.submitted&&(i=!1),k.filter_notsubmitted&&f.submitted&&(i=!1),k.filter_requiregrading&&!f.requiregrading&&(i=!1),k.filter_grantedextension&&!f.grantedextension&&(i=!1),i&&(b.each(e,function(a,b){"undefined"!=typeof f[b]&&""!==f[b]&&(g.hasidentity=!0,h.push(f[b]))}),g.identity=h.join(", "),d.push(c.render("mod_assign/list_participant_user_summary",g).then(function(a){return{value:f.id,label:a}})))}),b.when.apply(b,d)}).then(function(){var a=[];arguments[0]&&(a=Array.prototype.slice.call(arguments)),f(a)})["catch"](g)}}}); \ No newline at end of file +define(["core/ajax","jquery","core/templates"],function(a,b,c){return{processResults:function(a,b){return b},transport:function(d,e,f,g){var h=b(d).attr("data-assignmentid"),i=b(d).attr("data-groupid"),j=b('[data-region="configure-filters"] input[type="checkbox"]'),k=[];j.each(function(a,c){k[b(c).attr("name")]=b(c).prop("checked")}),a.call([{methodname:"mod_assign_list_participants",args:{assignid:h,groupid:i,filter:e,limit:30,includeenrolments:!1,tablesort:!0}}])[0].then(function(a){var d=[],e=b("[data-showuseridentity]").data("showuseridentity").split(",");return b.each(a,function(a,f){var g=f,h=[],i=!0;k.filter_submitted&&!f.submitted&&(i=!1),k.filter_notsubmitted&&f.submitted&&(i=!1),k.filter_requiregrading&&!f.requiregrading&&(i=!1),k.filter_grantedextension&&!f.grantedextension&&(i=!1),i&&(b.each(e,function(a,b){"undefined"!=typeof f[b]&&""!==f[b]&&(g.hasidentity=!0,h.push(f[b]))}),g.identity=h.join(", "),d.push(c.render("mod_assign/list_participant_user_summary",g).then(function(a){return{value:f.id,label:a}})))}),b.when.apply(b,d)}).then(function(){var a=[];arguments[0]&&(a=Array.prototype.slice.call(arguments)),f(a)})["catch"](g)}}}); \ No newline at end of file diff --git a/mod/assign/amd/src/grading_navigation.js b/mod/assign/amd/src/grading_navigation.js index f8f1758c344..9f6a349937a 100644 --- a/mod/assign/amd/src/grading_navigation.js +++ b/mod/assign/amd/src/grading_navigation.js @@ -38,10 +38,13 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', this._filters = []; this._users = []; this._filteredUsers = []; + this._lastXofYUpdate = 0; + this._firstLoadUsers = true; // Get the current user list from a webservice. this._loadAllUsers(); + // We do not allow navigation while ajax requests are pending. // Attach listeners to the select and arrow buttons. this._region.find('[data-action="previous-user"]').on('click', this._handlePreviousUser.bind(this)); @@ -56,7 +59,7 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', var toggleLink = this._region.find('[data-region="user-filters"]'); var configPanel = $(document.getElementById(toggleLink.attr('aria-controls'))); - configPanel.on('change', '[type="checkbox"]', this._filterChanged.bind(this)); + configPanel.on('change', 'select', this._filterChanged.bind(this)); var userid = $('[data-region="grading-navigation-panel"]').data('first-userid'); if (userid) { @@ -68,8 +71,6 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', } ).fail(notification.exception); - // We do not allow navigation while ajax requests are pending. - $(document).bind("start-loading-user", function() { this._isLoading = true; }.bind(this)); @@ -93,23 +94,44 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', /** @type {JQuery} JQuery node for the page region containing the user navigation. */ GradingNavigation.prototype._region = null; + /** @type {String} Last active filters */ + GradingNavigation.prototype._lastFilters = ''; + /** * Load the list of all users for this assignment. * * @private * @method _loadAllUsers + * @return {Boolean} True if the user list was fetched. */ GradingNavigation.prototype._loadAllUsers = function() { var select = this._region.find('[data-action=change-user]'); var assignmentid = select.attr('data-assignmentid'); var groupid = select.attr('data-groupid'); + var filterPanel = this._region.find('[data-region="configure-filters"]'); + var filter = filterPanel.find('select[name="filter"]').val(); + var workflowFilter = filterPanel.find('select[name="workflowfilter"]'); + if (workflowFilter) { + filter += ',' + workflowFilter.val(); + } + var markerFilter = filterPanel.find('select[name="markerfilter"]'); + if (markerFilter) { + filter += ',' + markerFilter.val(); + } + + if (this._lastFilters == filter) { + return false; + } + this._lastFilters = filter; + ajax.call([{ methodname: 'mod_assign_list_participants', - args: {assignid: assignmentid, groupid: groupid, filter: '', onlyids: true}, + args: {assignid: assignmentid, groupid: groupid, filter: '', onlyids: true, tablesort: true}, done: this._usersLoaded.bind(this), fail: notification.exception }]); + return true; }; /** @@ -120,13 +142,14 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', * @param {Array} users */ GradingNavigation.prototype._usersLoaded = function(users) { + this._firstLoadUsers = false; this._filteredUsers = this._users = users; if (this._users.length) { // Position the configure filters panel under the link that expands it. var toggleLink = this._region.find('[data-region="user-filters"]'); var configPanel = $(document.getElementById(toggleLink.attr('aria-controls'))); - configPanel.find('[type="checkbox"]').trigger('change'); + configPanel.find('select[name="filter"]').trigger('change'); } else { this._selectNoUser(); } @@ -153,6 +176,48 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', } }; + /** + * Close the configure filters panel if a click is detected outside of it. + * + * @private + * @method _updateFilterPreference + * @param {Number} userId The current user id. + * @param {Array} filterList The list of current filter values. + * @param {Array} preferenceNames The names of the preferences to update + * @return {Promise} Resolved when all the preferences are updated. + */ + GradingNavigation.prototype._updateFilterPreferences = function(userId, filterList, preferenceNames) { + var preferences = [], + i = 0; + + if (filterList.length == 0 || this._firstLoadUsers) { + // Nothing to update. + var deferred = $.Deferred(); + deferred.resolve(); + return deferred; + } + // General filter. + // Set the user preferences to the current filters. + for (i = 0; i < filterList.length; i++) { + var newValue = filterList[i]; + if (newValue == 'none') { + newValue = ''; + } + + preferences.push({ + userid: userId, + name: preferenceNames[i], + value: newValue + }); + } + + return ajax.call([{ + methodname: 'core_user_set_user_preferences', + args: { + preferences: preferences + } + }])[0]; + }; /** * Turn a filter on or off. * @@ -160,28 +225,20 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', * @method _filterChanged * @param {Event} event */ - GradingNavigation.prototype._filterChanged = function(event) { - var name = $(event.target).attr('name'); - var key = name.split('_').pop(); - var enabled = $(event.target).prop('checked'); + GradingNavigation.prototype._filterChanged = function() { + // There are 3 types of filter right now. + var filterPanel = this._region.find('[data-region="configure-filters"]'); + var filters = filterPanel.find('select'); - if (enabled) { - if (this._filters.indexOf(key) == -1) { - this._filters[this._filters.length] = key; - } - } else { - var index = this._filters.indexOf(key); - if (index != -1) { - this._filters.splice(index, 1); - } - } + this._filters = []; + filters.each(function(idx, ele) { + this._filters.push($(ele).val()); + }.bind(this)); // Update the active filter string. var filterlist = []; - this._region.find('[data-region="configure-filters"]').find('[type="checkbox"]').each(function(idx, ele) { - if ($(ele).prop('checked')) { - filterlist[filterlist.length] = $(ele).closest('label').text(); - } + filterPanel.find('option:checked').each(function(idx, ele) { + filterlist[filterlist.length] = $(ele).text(); }); if (filterlist.length) { this._region.find('[data-region="user-filters"] span').text(filterlist.join(', ')); @@ -191,50 +248,30 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', }.bind(this)).fail(notification.exception); } - // Filter the options in the select box that do not match the current filters. - var select = this._region.find('[data-action=change-user]'); - var userid = select.attr('data-selected'); - var foundIndex = 0; + var currentUserID = select.data('currentuserid'); + var preferenceNames = ['assign_filter', 'assign_workflowfilter', 'assign_markerfilter']; + this._updateFilterPreferences(currentUserID, this._filters, preferenceNames).done(function() { + // Reload the list of users to apply the new filters. + if (!this._loadAllUsers()) { + var userid = parseInt(select.attr('data-selected')); + var foundIndex = 0; + // Search the returned users for the current selection. + $.each(this._filteredUsers, function(index, user) { + if (userid == user.id) { + foundIndex = index; + } + }); - this._filteredUsers = []; - - $.each(this._users, function(index, user) { - var show = true; - $.each(this._filters, function(filterindex, filter) { - if (filter == "submitted") { - if (user.submitted == "0") { - show = false; - } - } else if (filter == "notsubmitted") { - if (user.submitted == "1") { - show = false; - } - } else if (filter == "requiregrading") { - if (user.requiregrading == "0") { - show = false; - } - } else if (filter == "grantedextension") { - if (user.grantedextension == "0") { - show = false; - } + if (this._filteredUsers.length) { + this._selectUserById(this._filteredUsers[foundIndex].id); + } else { + this._selectNoUser(); } - }); - if (show) { - this._filteredUsers[this._filteredUsers.length] = user; - if (userid == user.id) { - foundIndex = (this._filteredUsers.length - 1); - } } - }.bind(this)); - - if (this._filteredUsers.length) { - this._selectUserById(this._filteredUsers[foundIndex].id); - } else { - this._selectNoUser(); - } - this._triggerNextUserEvent(); + }.bind(this)).fail(notification.exception); + this._refreshCount(); }; /** @@ -396,6 +433,28 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', } }; + /** + * Set count string. This method only sets the value for the last time it was ever called to deal + * with promises that return in a non-predictable order. + * + * @private + * @method _setCountString + * @param {Number} x + * @param {Number} y + */ + GradingNavigation.prototype._setCountString = function(x, y) { + var updateNumber = 0; + this._lastXofYUpdate++; + updateNumber = this._lastXofYUpdate; + + var param = {x: x, y: y}; + str.get_string('xofy', 'mod_assign', param).done(function(s) { + if (updateNumber == this._lastXofYUpdate) { + this._region.find('[data-region="user-count-summary"]').text(s); + } + }.bind(this)).fail(notification.exception); + }; + /** * Rebuild the x of y string. * @@ -423,11 +482,19 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', if (count) { currentIndex += 1; } - var param = {x: currentIndex, y: count}; - - str.get_string('xofy', 'mod_assign', param).done(function(s) { - this._region.find('[data-region="user-count-summary"]').text(s); - }.bind(this)).fail(notification.exception); + this._setCountString(currentIndex, count); + // Update window URL + if (currentIndex > 0) { + var url = new URL(window.location); + if (parseInt(url.searchParams.get('blindid')) > 0) { + var newid = this._filteredUsers[currentIndex - 1].recordid; + url.searchParams.set('blindid', newid); + } else { + url.searchParams.set('userid', userid); + } + // We do this so a browser refresh will return to the same user. + window.history.replaceState({}, "", url); + } } }; diff --git a/mod/assign/amd/src/grading_navigation_user_info.js b/mod/assign/amd/src/grading_navigation_user_info.js index c07f40e598a..7d1b1702a3d 100644 --- a/mod/assign/amd/src/grading_navigation_user_info.js +++ b/mod/assign/amd/src/grading_navigation_user_info.js @@ -48,7 +48,7 @@ define(['jquery', 'core/notification', 'core/ajax', 'core/templates'], function( /** @type {JQuery} JQuery node for the page region containing the user navigation. */ UserInfo.prototype._region = null; - /** @type {Integer} Remember the last user id to prevent unnessecary reloads. */ + /** @type {Integer} Remember the last user id to prevent unnecessary reloads. */ UserInfo.prototype._lastUserId = 0; /** @@ -90,11 +90,13 @@ define(['jquery', 'core/notification', 'core/ajax', 'core/templates'], function( if (userid < 0) { // Render the template. templates.render('mod_assign/grading_navigation_no_users', {}).done(function(html, js) { - // Update the page. - this._region.fadeOut("fast", function() { - templates.replaceNodeContents(this._region, html, js); - this._region.fadeIn("fast"); - }.bind(this)); + if (userid == this._lastUserId) { + // Update the page. + this._region.fadeOut("fast", function() { + templates.replaceNodeContents(this._region, html, js); + this._region.fadeIn("fast"); + }.bind(this)); + } }.bind(this)).fail(notification.exception); return; } @@ -147,10 +149,12 @@ define(['jquery', 'core/notification', 'core/ajax', 'core/templates'], function( templates.render('mod_assign/grading_navigation_user_summary', context).done(function(html, js) { // Update the page. - this._region.fadeOut("fast", function() { - templates.replaceNodeContents(this._region, html, js); - this._region.fadeIn("fast"); - }.bind(this)); + if (userid == this._lastUserId) { + this._region.fadeOut("fast", function() { + templates.replaceNodeContents(this._region, html, js); + this._region.fadeIn("fast"); + }.bind(this)); + } }.bind(this)).fail(notification.exception); }.bind(this)).fail(function() { // Render the template. diff --git a/mod/assign/amd/src/participant_selector.js b/mod/assign/amd/src/participant_selector.js index 1323c77c09c..001b71128d0 100644 --- a/mod/assign/amd/src/participant_selector.js +++ b/mod/assign/amd/src/participant_selector.js @@ -59,7 +59,14 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) { ajax.call([{ methodname: 'mod_assign_list_participants', - args: {assignid: assignmentid, groupid: groupid, filter: query, limit: 30, includeenrolments: false} + args: { + assignid: assignmentid, + groupid: groupid, + filter: query, + limit: 30, + includeenrolments: false, + tablesort: true + } }])[0].then(function(results) { var promises = []; var identityfields = $('[data-showuseridentity]').data('showuseridentity').split(','); diff --git a/mod/assign/classes/output/grading_app.php b/mod/assign/classes/output/grading_app.php index c26f8701085..0e7e49d3e76 100644 --- a/mod/assign/classes/output/grading_app.php +++ b/mod/assign/classes/output/grading_app.php @@ -67,6 +67,9 @@ class grading_app implements templatable, renderable { $this->userid = $userid; $this->groupid = $groupid; $this->assignment = $assignment; + user_preference_allow_ajax_update('assign_filter', PARAM_ALPHA); + user_preference_allow_ajax_update('assign_workflowfilter', PARAM_ALPHA); + user_preference_allow_ajax_update('assign_markerfilter', PARAM_ALPHANUMEXT); $this->participants = $assignment->list_participants_with_filter_status_and_group($groupid); if (!$this->userid && count($this->participants)) { $this->userid = reset($this->participants)->id; @@ -80,7 +83,7 @@ class grading_app implements templatable, renderable { * @return stdClass - Flat list of exported data. */ public function export_for_template(renderer_base $output) { - global $CFG; + global $CFG, $USER; $export = new stdClass(); $export->userid = $this->userid; @@ -91,6 +94,12 @@ class grading_app implements templatable, renderable { $export->name = $this->assignment->get_context()->get_context_name(); $export->courseid = $this->assignment->get_course()->id; $export->participants = array(); + $export->filters = $this->assignment->get_filters(); + $export->markingworkflowfilters = $this->assignment->get_marking_workflow_filters(true); + $export->hasmarkingworkflow = count($export->markingworkflowfilters) > 0; + $export->markingallocationfilters = $this->assignment->get_marking_allocation_filters(true); + $export->hasmarkingallocation = count($export->markingallocationfilters) > 0; + $num = 1; foreach ($this->participants as $idx => $record) { $user = new stdClass(); @@ -160,6 +169,7 @@ class grading_app implements templatable, renderable { $export->larrow = $output->larrow(); // List of identity fields to display (the user info will not contain any fields the user cannot view anyway). $export->showuseridentity = $CFG->showuseridentity; + $export->currentuserid = $USER->id; return $export; } diff --git a/mod/assign/externallib.php b/mod/assign/externallib.php index 0c3b4bd02f9..35f7440b329 100644 --- a/mod/assign/externallib.php +++ b/mod/assign/externallib.php @@ -2546,7 +2546,9 @@ class mod_assign_external extends external_api { 'limit' => new external_value(PARAM_INT, 'maximum number of records to return', VALUE_DEFAULT, 0), 'onlyids' => new external_value(PARAM_BOOL, 'Do not return all user fields', VALUE_DEFAULT, false), 'includeenrolments' => new external_value(PARAM_BOOL, 'Do return courses where the user is enrolled', - VALUE_DEFAULT, true) + VALUE_DEFAULT, true), + 'tablesort' => new external_value(PARAM_BOOL, 'Apply current user table sorting preferences.', + VALUE_DEFAULT, false) ) ); } @@ -2561,11 +2563,13 @@ class mod_assign_external extends external_api { * @param int $limit Maximum number of records to return * @param bool $onlyids Only return user ids. * @param bool $includeenrolments Return courses where the user is enrolled. + * @param bool $tablesort Apply current user table sorting params from the grading table. * @return array of warnings and status result * @since Moodle 3.1 * @throws moodle_exception */ - public static function list_participants($assignid, $groupid, $filter, $skip, $limit, $onlyids, $includeenrolments) { + public static function list_participants($assignid, $groupid, $filter, $skip, + $limit, $onlyids, $includeenrolments, $tablesort) { global $DB, $CFG; require_once($CFG->dirroot . "/mod/assign/locallib.php"); require_once($CFG->dirroot . "/user/lib.php"); @@ -2578,7 +2582,8 @@ class mod_assign_external extends external_api { 'skip' => $skip, 'limit' => $limit, 'onlyids' => $onlyids, - 'includeenrolments' => $includeenrolments + 'includeenrolments' => $includeenrolments, + 'tablesort' => $tablesort )); $warnings = array(); @@ -2590,7 +2595,7 @@ class mod_assign_external extends external_api { $participants = array(); if (groups_group_visible($params['groupid'], $course, $cm)) { - $participants = $assign->list_participants_with_filter_status_and_group($params['groupid']); + $participants = $assign->list_participants_with_filter_status_and_group($params['groupid'], $params['tablesort']); } $userfields = user_get_default_fields(); @@ -2644,6 +2649,11 @@ class mod_assign_external extends external_api { if (!empty($record->groupname)) { $userdetails['groupname'] = $record->groupname; } + // Unique id is required for blind marking. + $userdetails['recordid'] = -1; + if (!empty($record->recordid)) { + $userdetails['recordid'] = $record->recordid; + } $result[] = $userdetails; } @@ -2675,6 +2685,7 @@ class mod_assign_external extends external_api { $userdesc->keys['profileimageurl']->required = VALUE_OPTIONAL; $userdesc->keys['email']->desc = 'Email address'; $userdesc->keys['idnumber']->desc = 'The idnumber of the user'; + $userdesc->keys['recordid'] = new external_value(PARAM_INT, 'record id'); // Define other keys. $otherkeys = [ diff --git a/mod/assign/lib.php b/mod/assign/lib.php index 718b7b46ec0..d9211a5c96a 100644 --- a/mod/assign/lib.php +++ b/mod/assign/lib.php @@ -2052,3 +2052,29 @@ function mod_assign_core_calendar_event_timestart_updated(\calendar_event $event $event->trigger(); } } + +/** + * Return a list of all the user preferences used by mod_assign. + * + * @return array + */ +function mod_assign_user_preferences() { + $preferences = array(); + $preferences['assign_filter'] = array( + 'type' => PARAM_ALPHA, + 'null' => NULL_NOT_ALLOWED, + 'default' => '' + ); + $preferences['assign_workflowfilter'] = array( + 'type' => PARAM_ALPHA, + 'null' => NULL_NOT_ALLOWED, + 'default' => '' + ); + $preferences['assign_markerfilter'] = array( + 'type' => PARAM_ALPHANUMEXT, + 'null' => NULL_NOT_ALLOWED, + 'default' => '' + ); + + return $preferences; +} diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 52ee1121193..724a6391038 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -33,11 +33,12 @@ define('ASSIGN_SUBMISSION_STATUS_DRAFT', 'draft'); define('ASSIGN_SUBMISSION_STATUS_SUBMITTED', 'submitted'); // Search filters for grading page. +define('ASSIGN_FILTER_NONE', 'none'); define('ASSIGN_FILTER_SUBMITTED', 'submitted'); define('ASSIGN_FILTER_NOT_SUBMITTED', 'notsubmitted'); define('ASSIGN_FILTER_SINGLE_USER', 'singleuser'); -define('ASSIGN_FILTER_REQUIRE_GRADING', 'require_grading'); -define('ASSIGN_FILTER_GRANTED_EXTENSION', 'granted_extension'); +define('ASSIGN_FILTER_REQUIRE_GRADING', 'requiregrading'); +define('ASSIGN_FILTER_GRANTED_EXTENSION', 'grantedextension'); // Marker filter for grading page. define('ASSIGN_MARKER_FILTER_NO_MARKER', -1); @@ -1936,11 +1937,12 @@ class assign { * If this is a group assignment, group info is also returned. * * @param int $currentgroup + * @param boolean $tablesort Apply current user table sorting preferences. * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension', * 'groupid', 'groupname' */ - public function list_participants_with_filter_status_and_group($currentgroup) { - $participants = $this->list_participants($currentgroup, false); + public function list_participants_with_filter_status_and_group($currentgroup, $tablesort = false) { + $participants = $this->list_participants($currentgroup, false, $tablesort); if (empty($participants)) { return $participants; @@ -1949,17 +1951,56 @@ class assign { } } + /** + * Return a valid order by segment for list_participants that matches + * the sorting of the current grading table. Not every field is supported, + * we are only concerned with a list of users so we can't search on anything + * that is not part of the user information (like grading statud or last modified stuff). + * + * @return string Order by clause for list_participants + */ + private function get_grading_sort_sql() { + $usersort = flexible_table::get_sort_for_table('mod_assign_grading'); + $extrauserfields = get_extra_user_fields($this->get_context()); + + $userfields = explode(',', user_picture::fields('', $extrauserfields)); + $orderfields = explode(',', $usersort); + $validlist = []; + + foreach ($orderfields as $orderfield) { + $orderfield = trim($orderfield); + foreach ($userfields as $field) { + $parts = explode(' ', $orderfield); + if ($parts[0] == $field) { + // Prepend the user table prefix and count this as a valid order field. + array_push($validlist, 'u.' . $orderfield); + } + } + } + // Produce a final list. + $result = implode(',', $validlist); + if (empty($result)) { + // Fall back ordering when none has been set. + $result = 'u.lastname, u.firstname, u.id'; + } + + return $result; + } + /** * Load a list of users enrolled in the current course with the specified permission and group. * 0 for no group. + * Apply any current sort filters from the grading table. * * @param int $currentgroup * @param bool $idsonly * @return array List of user records */ - public function list_participants($currentgroup, $idsonly) { + public function list_participants($currentgroup, $idsonly, $tablesort = false) { global $DB, $USER; + // Get the last known sort order for the grading table. + if (empty($currentgroup)) { $currentgroup = 0; } @@ -1971,6 +2012,7 @@ class assign { $fields = 'u.*'; $orderby = 'u.lastname, u.firstname, u.id'; + $additionaljoins = ''; $additionalfilters = ''; $instance = $this->get_instance(); @@ -1991,7 +2033,9 @@ class assign { // Note, different DBs have different ordering of NULL values. // Therefore we coalesce the current time into the timecreated field, and the max possible integer into // the ID field. - $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC"; + if (empty($tablesort)) { + $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC"; + } } if ($instance->markingworkflow && @@ -2026,6 +2070,19 @@ class assign { $this->participants[$key] = $users; } + if ($tablesort) { + // Resort the user list according to the grading table sort and filter settings. + $sortedfiltereduserids = $this->get_grading_userid_list(true, ''); + $sortedfilteredusers = []; + foreach ($sortedfiltereduserids as $nextid) { + $nextid = intval($nextid); + if (isset($this->participants[$key][$nextid])) { + $sortedfilteredusers[$nextid] = $this->participants[$key][$nextid]; + } + } + $this->participants[$key] = $sortedfilteredusers; + } + if ($idsonly) { $idslist = array(); foreach ($this->participants[$key] as $id => $user) { @@ -2320,9 +2377,21 @@ class assign { * Utility function to get the userid for every row in the grading table * so the order can be frozen while we iterate it. * + * @param boolean $cached If true, the cached list from the session could be returned. + * @param string $useridlistid String value used for caching the participant list. * @return array An array of userids */ - protected function get_grading_userid_list() { + protected function get_grading_userid_list($cached = false, $useridlistid = '') { + if ($cached) { + if (empty($useridlistid)) { + $useridlistid = $this->get_useridlist_key_id(); + } + $useridlistkey = $this->get_useridlist_key($useridlistid); + if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) { + $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(false, ''); + } + return $SESSION->mod_assign_useridlist[$useridlistkey]; + } $filter = get_user_preferences('assign_filter', ''); $table = new assign_grading_table($this, 0, $filter, 0, false); @@ -3935,11 +4004,7 @@ class assign { $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT); if (!$userid) { - $useridlistkey = $this->get_useridlist_key($useridlistid); - if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) { - $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(); - } - $useridlist = $SESSION->mod_assign_useridlist[$useridlistkey]; + $useridlist = $this->get_grading_userid_list(true, $useridlistid); } else { $rownum = 0; $useridlistid = 0; @@ -4233,13 +4298,7 @@ class assign { $markingworkflow = $this->get_instance()->markingworkflow; // Get marking states to show in form. - $markingworkflowoptions = array(); - if ($markingworkflow) { - $notmarked = get_string('markingworkflowstatenotmarked', 'assign'); - $markingworkflowoptions[''] = get_string('filternone', 'assign'); - $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked; - $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user()); - } + $markingworkflowoptions = $this->get_marking_workflow_filters(); // Print options for changing the filter and changing the number of results per page. $gradingoptionsformparams = array('cm'=>$cmid, @@ -6780,13 +6839,7 @@ class assign { } // Get marking states to show in form. - $markingworkflowoptions = array(); - if ($this->get_instance()->markingworkflow) { - $notmarked = get_string('markingworkflowstatenotmarked', 'assign'); - $markingworkflowoptions[''] = get_string('filternone', 'assign'); - $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked; - $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user()); - } + $markingworkflowoptions = $this->get_marking_workflow_filters(); $gradingoptionsparams = array('cm'=>$this->get_course_module()->id, 'contextid'=>$this->context->id, @@ -7278,11 +7331,7 @@ class assign { $bothids = ($userid && $useridlistid); if (!$userid || $bothids) { - $useridlistkey = $this->get_useridlist_key($useridlistid); - if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) { - $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(); - } - $useridlist = $SESSION->mod_assign_useridlist[$useridlistkey]; + $useridlist = $this->get_grading_userid_list(true, $useridlistid); } else { $useridlist = array($userid); $rownum = 0; @@ -8870,6 +8919,108 @@ class assign { public function set_most_recent_team_submission($submission) { $this->mostrecentteamsubmission = $submission; } + + /** + * Return array of valid grading allocation filters for the grading interface. + * + * @param boolean $export Export the list of filters for a template. + * @return array + */ + public function get_marking_allocation_filters($export = false) { + $markingallocation = $this->get_instance()->markingworkflow && + $this->get_instance()->markingallocation && + has_capability('mod/assign:manageallocations', $this->context); + // Get markers to use in drop lists. + $markingallocationoptions = array(); + if ($markingallocation) { + list($sort, $params) = users_order_by_sql('u'); + // Only enrolled users could be assigned as potential markers. + $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort); + $markingallocationoptions[''] = get_string('filternone', 'assign'); + $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign'); + $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context); + foreach ($markers as $marker) { + $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames); + } + } + if ($export) { + $allocationfilter = get_user_preferences('assign_markerfilter', ''); + $result = []; + foreach ($markingallocationoptions as $option => $label) { + array_push($result, [ + 'key' => $option, + 'name' => $label, + 'active' => ($allocationfilter == $option), + ]); + } + return $result; + } + return $markingworkflowoptions; + } + + /** + * Return array of valid grading workflow filters for the grading interface. + * + * @param boolean $export Export the list of filters for a template. + * @return array + */ + public function get_marking_workflow_filters($export = false) { + $markingworkflow = $this->get_instance()->markingworkflow; + // Get marking states to show in form. + $markingworkflowoptions = array(); + if ($markingworkflow) { + $notmarked = get_string('markingworkflowstatenotmarked', 'assign'); + $markingworkflowoptions[''] = get_string('filternone', 'assign'); + $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked; + $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user()); + } + if ($export) { + $workflowfilter = get_user_preferences('assign_workflowfilter', ''); + $result = []; + foreach ($markingworkflowoptions as $option => $label) { + array_push($result, [ + 'key' => $option, + 'name' => $label, + 'active' => ($workflowfilter == $option), + ]); + } + return $result; + } + return $markingworkflowoptions; + } + + /** + * Return array of valid search filters for the grading interface. + * + * @return array + */ + public function get_filters() { + $filterkeys = [ + ASSIGN_FILTER_SUBMITTED, + ASSIGN_FILTER_NOT_SUBMITTED, + ASSIGN_FILTER_REQUIRE_GRADING, + ASSIGN_FILTER_GRANTED_EXTENSION + ]; + + $current = get_user_preferences('assign_filter', ''); + + $filters = []; + // First is always "no filter" option. + array_push($filters, [ + 'key' => 'none', + 'name' => get_string('filternone', 'assign'), + 'active' => ($current == '') + ]); + + foreach ($filterkeys as $key) { + array_push($filters, [ + 'key' => $key, + 'name' => get_string('filter' . $key, 'assign'), + 'active' => ($current == $key) + ]); + } + return $filters; + } } /** diff --git a/mod/assign/styles.css b/mod/assign/styles.css index b0a37862303..57977b5ae29 100644 --- a/mod/assign/styles.css +++ b/mod/assign/styles.css @@ -354,14 +354,14 @@ .path-mod-assign [data-region="configure-filters"] { display: none; text-align: left; - width: auto; + width: 480px; background-color: #fff; background-clip: padding-box; box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); border-radius: 6px; position: absolute; margin-top: 28px; - margin-left: -140px; + margin-left: -452px; padding: 10px 0; z-index: 1; } @@ -391,11 +391,6 @@ border-bottom-color: #fff; } -.path-mod-assign [data-region="configure-filters"] label { - display: block; - padding: 3px 20px; -} - .path-mod-assign .alignment [data-region="configure-filters"] input { margin-bottom: 0; } @@ -978,7 +973,6 @@ left: auto; right: 15px; margin: 0; - height: 100%; line-height: 60px; } diff --git a/mod/assign/templates/grading_navigation_user_selector.mustache b/mod/assign/templates/grading_navigation_user_selector.mustache index 65d351951a6..f2283929cc8 100644 --- a/mod/assign/templates/grading_navigation_user_selector.mustache +++ b/mod/assign/templates/grading_navigation_user_selector.mustache @@ -23,7 +23,7 @@ * none Data attributes required for JS: - * data-action, data-assignmentid, data-groupid, data-region + * data-action, data-assignmentid, data-groupid, data-region, data-currentuserid Context variables required for this template: * see mod/assign/classes/output/grading_app.php @@ -31,37 +31,73 @@ This template uses ajax functionality, so it cannot be shown in the template library. }} {{{larrow}}} - - - -{{{rarrow}}} + + + + {{{rarrow}}}
- -{{#str}}xofy, mod_assign, { "x": "{{index}}", "y": "{{count}}" }{{/str}} - + + {{#str}}xofy, mod_assign, { "x": "{{index}}", "y": "{{count}}" }{{/str}} + - -
- - - - -
+ +
+
+ +
+ +
+
+ {{#hasmarkingallocation}} +
+ +
+ +
+
+ {{/hasmarkingallocation}} + {{#hasmarkingworkflow}} +
+ +
+ +
+
+ {{/hasmarkingworkflow}} +
diff --git a/mod/assign/tests/behat/grading_status.feature b/mod/assign/tests/behat/grading_status.feature index a06ab54a661..29b65fcd8e0 100644 --- a/mod/assign/tests/behat/grading_status.feature +++ b/mod/assign/tests/behat/grading_status.feature @@ -12,10 +12,12 @@ Feature: View the grading status of an assignment | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | + | student2 | C1 | student | @javascript Scenario: View the grading status for an assignment with marking workflow enabled @@ -46,6 +48,10 @@ Feature: View the grading status of an assignment And I navigate to "View all submissions" in current page administration And I should see "Not marked" in the "Student 1" "table_row" And I click on "Grade" "link" in the "Student 1" "table_row" + And I should see "1 of 2" + And I click on "Change filters" "link" + And I set the field "Filter" to "submitted" + And I should see "1 of 1" And I set the field "Grade out of 100" to "50" And I set the field "Marking workflow state" to "In review" And I set the field "Feedback comments" to "Great job! Lol, not really." @@ -71,6 +77,7 @@ Feature: View the grading status of an assignment And I navigate to "View all submissions" in current page administration And I should see "In review" in the "Student 1" "table_row" And I click on "Grade" "link" in the "Student 1" "table_row" + And I should see "1 of 1" And I set the field "Marking workflow state" to "Released" And I press "Save changes" And I press "Ok" @@ -93,6 +100,7 @@ Feature: View the grading status of an assignment And I navigate to "View all submissions" in current page administration And I should see "Released" in the "Student 1" "table_row" And I click on "Grade" "link" in the "Student 1" "table_row" + And I should see "1 of 1" And I set the field "Marking workflow state" to "In marking" And I set the field "Notify students" to "0" And I press "Save changes" @@ -104,6 +112,11 @@ Feature: View the grading status of an assignment # The grade should also remain displayed as it's stored in the assign DB tables, but the final grade should be empty. And "Student 1" row "Grade" column of "generaltable" table should contain "50.00" And "Student 1" row "Final grade" column of "generaltable" table should contain "-" + And I click on "Grade" "link" in the "Student 1" "table_row" + And I click on "Change filters" "link" + And I set the field "Workflow filter" to "In review" + And I should see "0 of 0" + And I follow "Test assignment name" And I log out @javascript @@ -134,6 +147,10 @@ Feature: View the grading status of an assignment And I navigate to "View all submissions" in current page administration And I should not see "Graded" in the "Student 1" "table_row" And I click on "Grade" "link" in the "Student 1" "table_row" + And I should see "1 of 2" + And I click on "Change filters" "link" + And I set the field "Filter" to "submitted" + And I should see "1 of 1" And I set the field "Grade out of 100" to "50" And I set the field "Feedback comments" to "Great job! Lol, not really." And I press "Save changes" @@ -167,6 +184,7 @@ Feature: View the grading status of an assignment And I should see "Graded - follow up submission received" in the "Student 1" "table_row" And I wait "10" seconds And I click on "Grade" "link" in the "Student 1" "table_row" + And I should see "1 of 1" And I set the field "Grade out of 100" to "99.99" And I set the field "Feedback comments" to "Even better job! Really." And I press "Save changes" diff --git a/mod/assign/tests/externallib_test.php b/mod/assign/tests/externallib_test.php index 6fa4ba1d694..f4dbf084c78 100644 --- a/mod/assign/tests/externallib_test.php +++ b/mod/assign/tests/externallib_test.php @@ -2425,7 +2425,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase { $DB->update_record('user', $student); $this->setUser($teacher); - $participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, true); + $participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, true, true); $participants = external_api::clean_returnvalue(mod_assign_external::list_participants_returns(), $participants); $this->assertCount(1, $participants); @@ -2443,7 +2443,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase { $this->assertEquals($student->institution, $participant['institution']); $this->assertArrayHasKey('enrolledcourses', $participant); - $participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, false); + $participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, false, true); $participants = external_api::clean_returnvalue(mod_assign_external::list_participants_returns(), $participants); // Check that the list of courses the participant is enrolled is not returned. $participant = $participants[0]; diff --git a/mod/assign/tests/locallib_test.php b/mod/assign/tests/locallib_test.php index 0c5c08704b5..602d0d94247 100644 --- a/mod/assign/tests/locallib_test.php +++ b/mod/assign/tests/locallib_test.php @@ -3987,4 +3987,17 @@ Anchor link 2:Link text // Check that submissionstatus_marked 'Graded' message does appear for student. $this->assertContains(get_string('submissionstatus_marked', 'assign'), $output2); } + + /** + * Test the result of get_filters is consistent. + */ + public function test_get_filters() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $assign = $this->create_instance($course); + $valid = $assign->get_filters(); + + $this->assertEquals(count($valid), 5); + } } diff --git a/theme/boost/templates/mod_assign/grading_navigation_user_selector.mustache b/theme/boost/templates/mod_assign/grading_navigation_user_selector.mustache index ab809172534..30378ce5f17 100644 --- a/theme/boost/templates/mod_assign/grading_navigation_user_selector.mustache +++ b/theme/boost/templates/mod_assign/grading_navigation_user_selector.mustache @@ -32,7 +32,7 @@ }} {{{larrow}}} - + {{{rarrow}}} @@ -44,12 +44,42 @@
- +
- - - - + + + + + {{#hasmarkingallocation}} + + + + + {{/hasmarkingallocation}} + {{#hasmarkingworkflow}} + + + + + {{/hasmarkingworkflow}}