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 | 203 +++++++++++------ .../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, 526 insertions(+), 159 deletions(-) diff --git a/mod/assign/amd/build/grading_navigation.min.js b/mod/assign/amd/build/grading_navigation.min.js index bc111f0250297..a2abc281fa1fc 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 d643460b97811..b80849665af59 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 7ad166abea3e3..072b89d417ce4 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 f8f1758c344be..9f6a349937a35 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'); - - 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); - } - } + 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'); + + 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; - - 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; + 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; } - } 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 (show) { - this._filteredUsers[this._filteredUsers.length] = user; - if (userid == user.id) { - foundIndex = (this._filteredUsers.length - 1); + if (this._filteredUsers.length) { + this._selectUserById(this._filteredUsers[foundIndex].id); + } else { + this._selectNoUser(); } - } - }.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 c07f40e598a72..7d1b1702a3d66 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 1323c77c09c02..001b71128d07f 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 c26f87010859d..0e7e49d3e76e2 100644 --- a/mod/assign/classes/output/grading_app.php +++ b/mod/assign/classes/output/grading_app.php @@ -67,6 +67,9 @@ public function __construct($userid, $groupid, $assignment) { $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 @@ public function __construct($userid, $groupid, $assignment) { * @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 @@ public function export_for_template(renderer_base $output) { $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 @@ public function export_for_template(renderer_base $output) { $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 0c3b4bd02f99a..35f7440b32980 100644 --- a/mod/assign/externallib.php +++ b/mod/assign/externallib.php @@ -2546,7 +2546,9 @@ public static function list_participants_parameters() { '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 @@ public static function list_participants_parameters() { * @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 @@ public static function list_participants($assignid, $groupid, $filter, $skip, $l 'skip' => $skip, 'limit' => $limit, 'onlyids' => $onlyids, - 'includeenrolments' => $includeenrolments + 'includeenrolments' => $includeenrolments, + 'tablesort' => $tablesort )); $warnings = array(); @@ -2590,7 +2595,7 @@ public static function list_participants($assignid, $groupid, $filter, $skip, $l $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 @@ public static function list_participants($assignid, $groupid, $filter, $skip, $l 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 @@ public static function list_participants_returns() { $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 718b7b46ec019..d9211a5c96a7e 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 52ee11211932b..724a639103851 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -33,11 +33,12 @@ 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 @@ private function get_submission_info_for_participants($participants) { * 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 @@ public function list_participants_with_filter_status_and_group($currentgroup) { } } + /** + * 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 @@ public function list_participants($currentgroup, $idsonly) { $fields = 'u.*'; $orderby = 'u.lastname, u.firstname, u.id'; + $additionaljoins = ''; $additionalfilters = ''; $instance = $this->get_instance(); @@ -1991,7 +2033,9 @@ public function list_participants($currentgroup, $idsonly) { // 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 @@ public function list_participants($currentgroup, $idsonly) { $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 @@ public function count_submissions_with_status($status, $currentgroup = null) { * 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 @@ protected function view_single_grade_page($mform) { $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 @@ protected function view_grading_table() { $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 @@ protected function process_save_grading_options() { } // 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 @@ public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $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 @@ protected function view_fix_rescaled_null_grades() { 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 b0a37862303c2..57977b5ae2974 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 65d351951a6ef..f2283929cc88d 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 a06ab54a66192..29b65fcd8e0d3 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 6fa4ba1d69417..f4dbf084c78c8 100644 --- a/mod/assign/tests/externallib_test.php +++ b/mod/assign/tests/externallib_test.php @@ -2425,7 +2425,7 @@ public function test_list_participants_user_info_with_special_characters() { $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 @@ public function test_list_participants_user_info_with_special_characters() { $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 0c5c08704b574..602d0d942473c 100644 --- a/mod/assign/tests/locallib_test.php +++ b/mod/assign/tests/locallib_test.php @@ -3987,4 +3987,17 @@ public function test_grade_submission_override() { // 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 ab8091725345e..30378ce5f1743 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}}