react gb: implement 'message students who'

closes CNVS-22024

test plan:

verify the 'Message Students Who...' dialog
works in the react gradebook AND in the normal
gradebook

Change-Id: Iec1201cc0a95816b9b9497a53a3d0c2c1406cacc
Reviewed-on: https://gerrit.instructure.com/64376
Tested-by: Jenkins
Reviewed-by: Dylan Ross <dross@instructure.com>
QA-Review: Jason Carter <jcarter@instructure.com>
Product-Review: Spencer Olson <solson@instructure.com>
This commit is contained in:
Spencer Olson 2015-09-29 17:08:25 -05:00
parent da6bcd1019
commit 05f776837c
11 changed files with 349 additions and 53 deletions

View File

@ -10,12 +10,13 @@ define [
'jst/re_upload_submissions_form'
'underscore'
'compiled/behaviors/authenticity_token'
'jsx/gradebook/grid/helpers/messageStudentsWhoHelper'
'jquery.instructure_forms'
'jqueryui/dialog'
'jquery.instructure_misc_helpers'
'jquery.instructure_misc_plugins'
'compiled/jquery.kylemenu'
], (I18n, $, messageStudents, AssignmentDetailsDialog, AssignmentMuter, SetDefaultGradeDialog, CurveGradesDialog, gradebookHeaderMenuTemplate, re_upload_submissions_form, _, authenticity_token) ->
], (I18n, $, messageStudents, AssignmentDetailsDialog, AssignmentMuter, SetDefaultGradeDialog, CurveGradesDialog, gradebookHeaderMenuTemplate, re_upload_submissions_form, _, authenticity_token, MessageStudentsWhoHelper) ->
class GradebookHeaderMenu
constructor: (@assignment, @$trigger, @gradebook) ->
@ -85,48 +86,8 @@ define [
score: sub?.score
submitted_at: sub?.submitted_at
submissionTypes = assignment.submission_types
hasSubmission = true
if submissionTypes.length == 0
hasSubmission = false
else if submissionTypes.length == 1
hasSubmission = not _.include(["none", "on_paper"], submissionTypes[0])
options = [
{text: I18n.t("students_who.havent_submitted_yet", "Haven't submitted yet")}
{text: I18n.t("students_who.havent_been_graded", "Haven't been graded")}
{text: I18n.t("students_who.scored_less_than", "Scored less than"), cutoff: true}
{text: I18n.t("students_who.scored_more_than", "Scored more than"), cutoff: true}
]
options.splice 0, 1 unless hasSubmission
window.messageStudents
options: options
title: assignment.name
points_possible: assignment.points_possible
students: students
context_code: "course_"+assignment.course_id
callback: (selected, cutoff, students) ->
students = $.grep students, ($student, idx) ->
student = $student.user_data
if selected == I18n.t("students_who.havent_submitted_yet", "Haven't submitted yet")
!student.submitted_at
else if selected == I18n.t("students_who.havent_been_graded", "Haven't been graded")
!student.score?
else if selected == I18n.t("students_who.scored_less_than", "Scored less than")
student.score? and student.score != "" and cutoff? and student.score < cutoff
else if selected == I18n.t("students_who.scored_more_than", "Scored more than")
student.score? and student.score != "" and cutoff? and student.score > cutoff
$.map students, (student) -> student.user_data.id
subjectCallback: (selected, cutoff) =>
cutoff = cutoff || ''
if selected == I18n.t('students_who.not_submitted_yet', "Haven't submitted yet")
I18n.t('students_who.no_submission_for', 'No submission for %{assignment}', assignment: assignment.name)
else if selected == I18n.t("students_who.havent_been_graded", "Haven't been graded")
I18n.t('students_who.no_grade_for', 'No grade for %{assignment}', assignment: assignment.name)
else if selected == I18n.t('students_who.scored_less_than', "Scored less than")
I18n.t('students_who.scored_less_than_on', 'Scored less than %{cutoff} on %{assignment}', assignment: assignment.name, cutoff: cutoff)
else if selected == I18n.t('students_who.scored_more_than', "Scored more than")
I18n.t('students_who.scored_more_than_on', 'Scored more than %{cutoff} on %{assignment}', assignment: assignment.name, cutoff: cutoff)
settings = MessageStudentsWhoHelper.settings(assignment, students)
messageStudents(settings)
setDefaultGrade: (opts={
assignment:@assignment,

View File

@ -92,6 +92,7 @@ define([
var columnType = columnData.columnType,
assignment = columnData.assignment,
enrollments = columnData.enrollments,
submissions = columnData.submissions,
key, dropdownOptionsId;
if (assignment) {
key = 'assignment-' + assignment.id;
@ -101,7 +102,8 @@ define([
idToAppendTo='gradebook_grid' screenreaderText={I18n.t('Assignment Options')}
defaultClassNames='gradebook-header-drop' options={{ noButton: true }}>
<AssignmentHeaderDropdownOptions key={dropdownOptionsId}
idAttribute={dropdownOptionsId} assignment={assignment} enrollments={enrollments}/>
idAttribute={dropdownOptionsId} assignment={assignment}
enrollments={enrollments} submissions={submissions}/>
</GradebookKyleMenu>
);
} else if (columnType === 'total') {

View File

@ -5,13 +5,15 @@ define([
'jsx/gradebook/grid/components/dropdown_components/headerDropdownOption',
'jsx/gradebook/grid/constants',
'jsx/gradebook/grid/components/dropdown_components/setDefaultGradeOption',
'jsx/gradebook/grid/components/dropdown_components/muteAssignmentOption'
], function (React, _, I18n, HeaderDropdownOption, GradebookConstants, SetDefaultGradeOption, MuteAssignmentOption) {
'jsx/gradebook/grid/components/dropdown_components/muteAssignmentOption',
'jsx/gradebook/grid/components/dropdown_components/messageStudentsWhoOption'
], function (React, _, I18n, HeaderDropdownOption, GradebookConstants, SetDefaultGradeOption, MuteAssignmentOption, MessageStudentsWhoOption) {
var AssignmentHeaderDropdownOptions = React.createClass({
propTypes: {
assignment: React.PropTypes.object.isRequired,
submissions: React.PropTypes.object.isRequired,
idAttribute: React.PropTypes.string.isRequired,
enrollments: React.PropTypes.array.isRequired
},
@ -35,7 +37,7 @@ define([
dropdownOptions.push(downloadSubmissionsOption);
}
if (GradebookConstants.gradebook_is_editable &&assignment.submissions_downloads > 0 ) {
if (GradebookConstants.gradebook_is_editable && assignment.submissions_downloads > 0 ) {
reuploadSubmissionsOption = { title: I18n.t('Re-Upload Submissions'), action: 'reuploadSubmissions' };
dropdownOptions.push(reuploadSubmissionsOption);
}
@ -53,6 +55,7 @@ define([
var options = this.getDropdownOptions(),
assignment = this.props.assignment,
enrollments = this.props.enrollments,
submissions = this.props.submissions,
key;
return (
<ul id={this.props.idAttribute} className="gradebook-header-menu">
@ -70,6 +73,12 @@ define([
enrollments={enrollments}
contextId={GradebookConstants.context_id}/>
);
} else if (listItem.action === 'messageStudentsWho') {
return (
<MessageStudentsWhoOption
key={key} title={listItem.title} assignment={assignment}
enrollments={enrollments} submissions={submissions}/>
);
} else {
return(
<HeaderDropdownOption

View File

@ -0,0 +1,48 @@
define([
'react',
'underscore',
'jsx/gradebook/grid/components/dropdown_components/headerDropdownOption',
'jsx/gradebook/grid/helpers/submissionsHelper',
'timezone',
'jsx/gradebook/grid/helpers/messageStudentsWhoHelper',
'message_students'
], function (React, _, HeaderDropdownOption, SubmissionsHelper, tz, MessageStudentsWhoHelper) {
var MessageStudentsWhoOption = React.createClass({
propTypes: {
title: React.PropTypes.string.isRequired,
assignment: React.PropTypes.object.isRequired,
enrollments: React.PropTypes.array.isRequired,
submissions: React.PropTypes.object.isRequired
},
openDialog() {
var submissions = SubmissionsHelper.
submissionsForAssignment(this.props.submissions, this.props.assignment.id);
var students = _.map(submissions, (submission) => {
submission.submitted_at = tz.parse(submission.submitted_at);
var enrollment = _.find(
this.props.enrollments,
enrollment => enrollment.user_id === submission.user_id
);
if(enrollment) submission.name = enrollment.user.name;
return submission;
});
var settings = MessageStudentsWhoHelper.settings(this.props.assignment, students);
messageStudents(settings);
},
render() {
return(
<HeaderDropdownOption
handleClick={this.openDialog}
key={'messageStudentsWho' + this.props.assignment.id}
title={this.props.title}/>
);
}
});
return MessageStudentsWhoOption;
});

View File

@ -21,7 +21,8 @@ define([
'jsx/gradebook/grid/stores/tableStore',
'jsx/gradebook/grid/actions/sectionsActions',
'jsx/gradebook/grid/helpers/columnArranger',
'vendor/spin'
'vendor/spin',
'jsx/gradebook/grid/helpers/submissionsHelper'
], function (
React,
FixedDataTable,
@ -45,7 +46,8 @@ define([
TableStore,
SectionsActions,
ColumnArranger,
Spinner
Spinner,
SubmissionsHelper
){
var Table = FixedDataTable.Table,
Column = FixedDataTable.Column,
@ -150,12 +152,14 @@ define([
var columnIdentifier = columnId || columnType,
columnWidth = this.getColumnWidth(columnIdentifier),
enrollments = this.state.tableData.students,
submissions = this.state.tableData.submissions,
columnData = {
columnType: columnType,
activeCell: this.state.currentCellIndex,
setActiveCell: KeyboardNavigationActions.setActiveCell,
assignment: assignment,
enrollments: enrollments
enrollments: enrollments,
submissions: submissions
};
return (

View File

@ -0,0 +1,90 @@
define([
'underscore',
'i18n!gradebook2',
], function (_, I18n) {
var MessageStudentsWhoHelper = {
settings: function(assignment, students) {
return {
options: this.options(assignment),
title: assignment.name,
points_possible: assignment.points_possible,
students: students,
context_code: "course_" + assignment.course_id,
callback: this.callbackFn.bind(this),
subjectCallback: this.generateSubjectCallbackFn(assignment)
}
},
options: function(assignment) {
var options = this.allOptions();
var noSubmissions = !this.hasSubmission(assignment);
if(noSubmissions) options.splice(0,1);
return options;
},
allOptions: function() {
return [
{
text: I18n.t("students_who.havent_submitted_yet", "Haven't submitted yet"),
subjectFn: (assignment) => I18n.t('students_who.no_submission_for', 'No submission for %{assignment}', { assignment: assignment.name }),
criteriaFn: (student) => !student.submitted_at
},
{
text: I18n.t("students_who.havent_been_graded", "Haven't been graded"),
subjectFn: (assignment) => I18n.t('students_who.no_grade_for', 'No grade for %{assignment}', { assignment: assignment.name }),
criteriaFn: (student) => !this.exists(student.score)
},
{
text: I18n.t("students_who.scored_less_than", "Scored less than"),
cutoff: true,
subjectFn: (assignment, cutoff) => I18n.t('students_who.scored_less_than_on', 'Scored less than %{cutoff} on %{assignment}', { assignment: assignment.name, cutoff: cutoff }),
criteriaFn: (student, cutoff) => this.scoreWithCutoff(student, cutoff) && student.score < cutoff
},
{
text: I18n.t("students_who.scored_more_than", "Scored more than"),
cutoff: true,
subjectFn: (assignment, cutoff) => I18n.t('students_who.scored_more_than_on', 'Scored more than %{cutoff} on %{assignment}', { assignment: assignment.name, cutoff: cutoff }),
criteriaFn: (student, cutoff) => this.scoreWithCutoff(student, cutoff) && student.score > cutoff
}
];
},
hasSubmission: function(assignment) {
var submissionTypes = assignment.submission_types;
if(submissionTypes.length === 0) return false;
return _.any(submissionTypes, (submissionType) => {
return submissionType !== 'none' && submissionType !== 'on_paper';
});
},
exists: function(value) {
return !_.isUndefined(value) && !_.isNull(value);
},
scoreWithCutoff: function(student, cutoff) {
return this.exists(student.score)
&& student.score !== ''
&& this.exists(cutoff);
},
callbackFn: function(selected, cutoff, students) {
var criteriaFn = this.findOptionByText(selected).criteriaFn;
var students = _.filter(students, student => criteriaFn(student.user_data, cutoff));
return _.map(students, student => student.user_data.id);
},
findOptionByText: function(text) {
return _.find(this.allOptions(), option => option.text === text);
},
generateSubjectCallbackFn: function(assignment) {
return (selected, cutoff) => {
var cutoffString = cutoff || '';
var subjectFn = this.findOptionByText(selected).subjectFn;
return subjectFn(assignment, cutoffString);
}
}
};
return MessageStudentsWhoHelper;
});

View File

@ -0,0 +1,23 @@
define([
'underscore',
], function (_) {
var SubmissionsHelper = {
submissionsForAssignment: function(submissionGroups, assignmentId) {
var submissions = this.extractSubmissions(submissionGroups);
return _.filter(
submissions,
submission => submission.assignment_id.toString() === assignmentId.toString()
);
},
extractSubmissions: function(submissionGroups) {
return _.chain(submissionGroups)
.values()
.flatten()
.pluck('submissions')
.flatten()
.value();
}
};
return SubmissionsHelper;
});

View File

@ -54,7 +54,8 @@ define [
deepEqual displayedDueDate(component), 'No due date'
test 'displays the override due date if the assignment has no due date and one' +
'override with a due date', ->
' override with a due date', ->
@stub($, 'sameYear').returns(true)
props = defaultProps()
props.columnData.assignment.due_at = null
props.columnData.assignment.overrides = [

View File

@ -0,0 +1,38 @@
define [
'jsx/gradebook/grid/components/dropdown_components/messageStudentsWhoOption'
'jsx/gradebook/grid/helpers/submissionsHelper'
'jsx/gradebook/grid/helpers/messageStudentsWhoHelper'
'timezone'
], (MessageStudentsWhoOption, SubmissionsHelper, MessageStudentsWhoHelper, tz) ->
wrapper = document.getElementById('fixtures')
renderComponent = ->
assignment = { id: '1', }
props =
title: 'Message Students Who...'
assignment: assignment
enrollments: [{ course_id: '1', user_id: '3', user: { id: '3', name: 'Dora' } }]
submissions: {}
componentFactory = React.createFactory(MessageStudentsWhoOption)
React.render(componentFactory(props), wrapper)
module 'MessageStudentsWhoOption',
setup: ->
@component = renderComponent()
teardown: ->
React.unmountComponentAtNode wrapper
test 'mounts on build', ->
ok renderComponent().isMounted()
test 'openDialog calls MessageStudentsWhoHelper#settings with the correct arguments', ->
@stub SubmissionsHelper, 'submissionsForAssignment', ->
[{ user_id: '3', submitted_at: '2014-04-20T00:00:00Z' }]
@stub window, 'messageStudents'
settingsStub = @stub MessageStudentsWhoHelper, 'settings'
@component.openDialog()
ok settingsStub.calledOnce
expectedStudents = [{ user_id: "3", submitted_at: tz.parse('2014-04-20T00:00:00Z'), name: "Dora" }]
deepEqual settingsStub.args[0][0], @component.props.assignment
deepEqual settingsStub.args[0][1], expectedStudents

View File

@ -1,8 +1,7 @@
define [
'react'
'jsx/gradebook/grid/helpers/datesHelper'
'underscore'
], (React, DatesHelper, _) ->
], (DatesHelper, _) ->
defaultAssignment = ->
{

View File

@ -0,0 +1,121 @@
define [
'jsx/gradebook/grid/helpers/messageStudentsWhoHelper'
'underscore'
], (MessageStudentsWhoHelper, _) ->
module "messageStudentsWhoHelper#options",
setup: ->
@assignment = { id: '1', name: 'Shootbags'}
test "Includes the 'Haven't been graded' option if there are submissions", ->
@stub(MessageStudentsWhoHelper, 'hasSubmission', -> true)
options = MessageStudentsWhoHelper.options(@assignment)
deepEqual options[1].text, "Haven't been graded"
test "Does not include the 'Haven't been graded' option if there are no submissions", ->
@stub(MessageStudentsWhoHelper, 'hasSubmission', -> false)
options = MessageStudentsWhoHelper.options(@assignment)
deepEqual options[1].text, "Scored less than"
module "messageStudentsWhoHelper#hasSubmission"
test "returns false if there are no submission types", ->
assignment = { id: '1', name: 'Shootbags', submission_types: [] }
hasSubmission = MessageStudentsWhoHelper.hasSubmission(assignment)
deepEqual hasSubmission, false
test "returns false if the only submission type is 'none'", ->
assignment = { id: '1', name: 'Shootbags', submission_types: ['none'] }
hasSubmission = MessageStudentsWhoHelper.hasSubmission(assignment)
deepEqual hasSubmission, false
test "returns false if the only submission type is 'on_paper'", ->
assignment = { id: '1', name: 'Shootbags', submission_types: ['on_paper'] }
hasSubmission = MessageStudentsWhoHelper.hasSubmission(assignment)
deepEqual hasSubmission, false
test "returns false if the only submission types are 'none' and 'on_paper'", ->
assignment = { id: '1', name: 'Shootbags', submission_types: ['none', 'on_paper'] }
hasSubmission = MessageStudentsWhoHelper.hasSubmission(assignment)
deepEqual hasSubmission, false
test "returns true if there is at least one submission that is not of type 'non' or 'on_paper'", ->
assignment = { id: '1', name: 'Shootbags', submission_types: ['online_quiz'] }
hasSubmission = MessageStudentsWhoHelper.hasSubmission(assignment)
deepEqual hasSubmission, true
module "messageStudentsWhoHelper#scoreWithCutoff"
test "returns true if the student has a non-empty-string score and a cutoff", ->
student = { score: 6 }
cutoff = 5
scoreWithCutoff = MessageStudentsWhoHelper.scoreWithCutoff(student, cutoff)
deepEqual scoreWithCutoff, true
test "returns false if the student has an empty-string score", ->
student = { score: '' }
cutoff = 5
scoreWithCutoff = MessageStudentsWhoHelper.scoreWithCutoff(student, cutoff)
deepEqual scoreWithCutoff, false
test "returns false if the student score is null or undefined", ->
student = {}
cutoff = 5
scoreWithCutoff = MessageStudentsWhoHelper.scoreWithCutoff(student, cutoff)
deepEqual scoreWithCutoff, false
student.score = null
scoreWithCutoff = MessageStudentsWhoHelper.scoreWithCutoff(student, cutoff)
deepEqual scoreWithCutoff, false
test "returns false if the cutoff is null or undefined", ->
student = { score: 5 }
scoreWithCutoff = MessageStudentsWhoHelper.scoreWithCutoff(student, cutoff)
deepEqual scoreWithCutoff, false
cutoff = null
scoreWithCutoff = MessageStudentsWhoHelper.scoreWithCutoff(student, cutoff)
deepEqual scoreWithCutoff, false
module 'messageStudentsWhoHelper#callbackFn'
test "returns the student ids filtered by the correct criteria", ->
option = { criteriaFn: (student, cutoff) -> student.score > cutoff }
@stub(MessageStudentsWhoHelper, 'findOptionByText', -> option)
students = [{ user_data: { id: '1', score: 8 } }, { user_data: { id: '2', score: 4 } }]
cutoff = 5
selected = "Scored more than"
filteredStudents = MessageStudentsWhoHelper.callbackFn(selected, cutoff, students)
deepEqual filteredStudents.length, 1
deepEqual filteredStudents[0], '1'
module 'messageStudentsWhoHelper#generateSubjectCallbackFn'
test "generates a function that returns the subject string", ->
option = { subjectFn: (assignment, cutoff) -> 'name: ' + assignment.name + ', cutoff: ' + cutoff }
@stub(MessageStudentsWhoHelper, 'findOptionByText', -> option)
assignment = { id: '1', name: 'Shootbags' }
cutoff = 5
subjectCallbackFn = MessageStudentsWhoHelper.generateSubjectCallbackFn(assignment)
deepEqual subjectCallbackFn(assignment,cutoff), 'name: Shootbags, cutoff: 5'
module 'messageStudentsWhoHelper#settings'
test "returns an object with the expected settings", ->
assignment =
id: '1'
name: 'Shootbags'
points_possible: 5
course_id: '5'
students = [{ id: '1', name: 'Dora' }]
self =
options: -> 'stuff'
callbackFn: -> 'call me back!'
generateSubjectCallbackFn: -> -> 'function inception'
settingsFn = MessageStudentsWhoHelper.settings.bind(self)
settings = settingsFn(assignment, students)
settingsKeys = _.keys settings
expectedKeys = ["options", "title", "points_possible",
"students", "context_code", "callback", "subjectCallback"]
deepEqual settingsKeys, expectedKeys