allow submissions with comments to be posted

Submissions without grades, but with comments from instructors, will
now be postable.

closes APG-102

Test Plan
- Enable the Allow Postable Submission Comments feature flag.
- Create a manually posted assignment (NORMAL).
- Create a peer review, manually posted assignment (PEER).
- Create a group, manually posted assignment (GROUP).

NORMAL
- Verify that the option to post grades for the assignment is
  disabled, in both Gradebook and SpeedGrader.
- As a teacher, leave a comment on a student's submission.
- Reload the Gradebook.
- Verify that the option to post grades is available for both
  Gradebook and SpeedGrader.
- Verify posting grades works.
- Verify hiding grades works.

- After hiding grades, post grades for Graded only.
- Verify that the submission with a comment is posted.
- Verify that submissions with no comments and no grades remain
  unposted.

- Hide grades.
- Change the manually posted assignment to be automatically posted.
- Verify posting grades works.
- Verify hiding grades works.

- Disable the Allow Postable Submission Comments feature flag.
- Verify that submission comments on a manual posting assignment are
  not enough to allow posting of the assignment.

PEER
- Set up peer reviews between two students.
- Submit to the assignment for each student.
- Comment as students for self, and for the peer review.
- Verify that the option to post grades is not available for both
  Gradebook and SpeedGrader.
- Comment as a teacher on one of the submissions.
- Verify that the option to post grades is available for both
  Gradebook and SpeedGrader.

GROUP
- Set up a group with at least 2 students.
- Set up a group assignment.
- As one student, submit to the assignment and leave a comment.
- As the other student in the group, leave a comment.
- Verify that the option to post grades is not available for both
  Gradebook and SpeedGrader.
- Comment as a teacher on one of the submissions.
- Verify that the option to post grades is available for both
  Gradebook and SpeedGrader.

Change-Id: Ia33b757d6c0acdb8cb915f980e81b3b485abc61a
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/208747
Tested-by: Jenkins
Reviewed-by: Adrian Packel <apackel@instructure.com>
Reviewed-by: Spencer Olson <solson@instructure.com>
QA-Review: Adrian Packel <apackel@instructure.com>
Product-Review: Jonathan Fenton <jfenton@instructure.com>
This commit is contained in:
Gary Mei 2019-10-18 11:04:37 -05:00
parent fd70765313
commit 6f3e62a013
37 changed files with 551 additions and 208 deletions

View File

@ -75,7 +75,7 @@ import assignmentHelper from 'jsx/gradezilla/shared/helpers/assignmentHelper'
import TextMeasure from 'jsx/gradezilla/shared/helpers/TextMeasure' import TextMeasure from 'jsx/gradezilla/shared/helpers/TextMeasure'
import * as GradeInputHelper from 'jsx/grading/helpers/GradeInputHelper' import * as GradeInputHelper from 'jsx/grading/helpers/GradeInputHelper'
import OutlierScoreHelper from 'jsx/grading/helpers/OutlierScoreHelper' import OutlierScoreHelper from 'jsx/grading/helpers/OutlierScoreHelper'
import {isHidden} from 'jsx/grading/helpers/SubmissionHelper' import {isPostable} from 'jsx/grading/helpers/SubmissionHelper'
import LatePolicyApplicator from 'jsx/grading/LatePolicyApplicator' import LatePolicyApplicator from 'jsx/grading/LatePolicyApplicator'
import {Button} from '@instructure/ui-buttons' import {Button} from '@instructure/ui-buttons'
import {IconSettingsSolid} from '@instructure/ui-icons' import {IconSettingsSolid} from '@instructure/ui-icons'
@ -2088,7 +2088,7 @@ export default do ->
# Ignore anonymous assignments when deciding whether to show the # Ignore anonymous assignments when deciding whether to show the
# "hidden" icon, as including them could reveal which students have # "hidden" icon, as including them could reveal which students have
# and have not been graded # and have not been graded
submission? && isHidden(submission) && !assignment.anonymize_students submission? && isPostable(submission) && !assignment.anonymize_students
) )
else else
@filteredContentInfo.mutedAssignments @filteredContentInfo.mutedAssignments

View File

@ -472,6 +472,10 @@ class SubmissionsApiController < ApplicationController
end end
if params[:grouped].present? if params[:grouped].present?
if @context.root_account.feature_enabled?(:allow_postable_submission_comments) && @context.post_policies_enabled?
includes << "has_postable_comments"
end
scope = (@section || @context).all_student_enrollments. scope = (@section || @context).all_student_enrollments.
preload(:root_account, :sis_pseudonym, :user => :pseudonyms). preload(:root_account, :sis_pseudonym, :user => :pseudonyms).
where(:user_id => student_ids).order(:user_id) where(:user_id => student_ids).order(:user_id)
@ -483,8 +487,17 @@ class SubmissionsApiController < ApplicationController
if params[:workflow_state].present? if params[:workflow_state].present?
submissions_scope = submissions_scope.where(:workflow_state => params[:workflow_state]) submissions_scope = submissions_scope.where(:workflow_state => params[:workflow_state])
end end
submissions_scope = submissions_scope.preload(:attachment) unless params[:exclude_response_fields]&.include?('attachments')
submissions = submissions_scope.preload(:originality_reports, :quiz_submission).to_a submission_preloads = [:originality_reports, :quiz_submission]
submission_preloads << :attachment unless params[:exclude_response_fields]&.include?("attachments")
submissions = submissions_scope.preload(submission_preloads).to_a
ActiveRecord::Associations::Preloader.new.preload(
submissions,
:submission_comments,
{select: [:hidden, :submission_id]}
)
bulk_load_attachments_and_previews(submissions) bulk_load_attachments_and_previews(submissions)
submissions_for_user = submissions.group_by(&:user_id) submissions_for_user = submissions.group_by(&:user_id)

View File

@ -58,9 +58,15 @@ class Mutations::PostAssignmentGrades < Mutations::BaseMutation
visible_enrollments = visible_enrollments.where(user_id: input[:only_student_ids]) if input[:only_student_ids] visible_enrollments = visible_enrollments.where(user_id: input[:only_student_ids]) if input[:only_student_ids]
visible_enrollments = visible_enrollments.where.not(user_id: input[:skip_student_ids]) if input[:skip_student_ids] visible_enrollments = visible_enrollments.where.not(user_id: input[:skip_student_ids]) if input[:skip_student_ids]
submissions_scope = input[:graded_only] ? assignment.submissions.graded : assignment.submissions submissions_scope = if input[:graded_only] && course.root_account.feature_enabled?(:allow_postable_submission_comments)
submissions_scope = submissions_scope.joins(user: :enrollments).merge(visible_enrollments) assignment.submissions.postable
elsif input[:graded_only]
assignment.submissions.graded
else
assignment.submissions
end
submissions_scope = submissions_scope.joins(user: :enrollments).merge(visible_enrollments)
submission_ids = submissions_scope.pluck(:id) submission_ids = submissions_scope.pluck(:id)
progress = course.progresses.new(tag: "post_assignment_grades") progress = course.progresses.new(tag: "post_assignment_grades")

View File

@ -50,7 +50,14 @@ class Mutations::PostAssignmentGradesForSections < Mutations::BaseMutation
visible_enrollments = course.apply_enrollment_visibility(course.student_enrollments, current_user, sections) visible_enrollments = course.apply_enrollment_visibility(course.student_enrollments, current_user, sections)
submissions_scope = input[:graded_only] ? assignment.submissions.graded : assignment.submissions submissions_scope = if input[:graded_only] && course.root_account.feature_enabled?(:allow_postable_submission_comments)
assignment.submissions.postable
elsif input[:graded_only]
assignment.submissions.graded
else
assignment.submissions
end
submissions_scope = submissions_scope.joins(user: :enrollments).merge(visible_enrollments) submissions_scope = submissions_scope.joins(user: :enrollments).merge(visible_enrollments)
progress = course.progresses.new(tag: "post_assignment_grades_for_sections") progress = course.progresses.new(tag: "post_assignment_grades_for_sections")

View File

@ -22,7 +22,7 @@ import I18n from 'i18n!gradezilla'
import htmlEscape from 'str/htmlEscape' import htmlEscape from 'str/htmlEscape'
import {extractDataTurnitin} from 'compiled/gradezilla/Turnitin' import {extractDataTurnitin} from 'compiled/gradezilla/Turnitin'
import GradeFormatHelper from '../../../../gradebook/shared/helpers/GradeFormatHelper' import GradeFormatHelper from '../../../../gradebook/shared/helpers/GradeFormatHelper'
import {extractSimilarityInfo, isHidden} from '../../../../grading/helpers/SubmissionHelper' import {extractSimilarityInfo, isPostable} from '../../../../grading/helpers/SubmissionHelper'
import {classNamesForAssignmentCell} from './CellStyles' import {classNamesForAssignmentCell} from './CellStyles'
function getTurnitinState(submission) { function getTurnitinState(submission) {
@ -214,7 +214,7 @@ export default class AssignmentCellFormatter {
} }
const showUnpostedIndicator = const showUnpostedIndicator =
columnDef.postAssignmentGradesTrayOpenForAssignmentId && isHidden(submission) columnDef.postAssignmentGradesTrayOpenForAssignmentId && isPostable(submission)
const options = { const options = {
classNames: classNamesForAssignmentCell(assignmentData, submissionData), classNames: classNamesForAssignmentCell(assignmentData, submissionData),

View File

@ -26,7 +26,7 @@ import {Text} from '@instructure/ui-elements'
import 'message_students' import 'message_students'
import I18n from 'i18n!gradezilla' import I18n from 'i18n!gradezilla'
import {ScreenReaderContent} from '@instructure/ui-a11y' import {ScreenReaderContent} from '@instructure/ui-a11y'
import {isHidden} from '../../../../grading/helpers/SubmissionHelper' import {isPostable} from '../../../../grading/helpers/SubmissionHelper'
import MessageStudentsWhoHelper from '../../../shared/helpers/messageStudentsWhoHelper' import MessageStudentsWhoHelper from '../../../shared/helpers/messageStudentsWhoHelper'
import ColumnHeader from './ColumnHeader' import ColumnHeader from './ColumnHeader'
@ -89,23 +89,23 @@ SecondaryDetailLine.propTypes = {
} }
function labelForPostGradesAction(postGradesAction) { function labelForPostGradesAction(postGradesAction) {
if (!postGradesAction.hasGrades) { if (postGradesAction.hasGradesOrCommentsToPost) {
return I18n.t('No grades to post')
} else if (postGradesAction.hasGradesToPost) {
return I18n.t('Post grades') return I18n.t('Post grades')
} else if (postGradesAction.hasGradesOrPostableComments) {
return I18n.t('All grades posted')
} }
return I18n.t('All grades posted') return I18n.t('No grades to post')
} }
function labelForHideGradesAction(hideGradesAction) { function labelForHideGradesAction(hideGradesAction) {
if (!hideGradesAction.hasGrades) { if (hideGradesAction.hasGradesOrCommentsToHide) {
return I18n.t('No grades to hide')
} else if (hideGradesAction.hasGradesToHide) {
return I18n.t('Hide grades') return I18n.t('Hide grades')
} else if (hideGradesAction.hasGradesOrPostableComments) {
return I18n.t('All grades hidden')
} }
return I18n.t('All grades hidden') return I18n.t('No grades to hide')
} }
export default class AssignmentColumnHeader extends ColumnHeader { export default class AssignmentColumnHeader extends ColumnHeader {
@ -131,13 +131,14 @@ export default class AssignmentColumnHeader extends ColumnHeader {
}).isRequired, }).isRequired,
hideGradesAction: shape({ hideGradesAction: shape({
hasGradesToHide: bool.isRequired, hasGradesOrCommentsToHide: bool.isRequired,
onSelect: func.isRequired onSelect: func.isRequired
}).isRequired, }).isRequired,
postGradesAction: shape({ postGradesAction: shape({
featureEnabled: bool.isRequired, featureEnabled: bool.isRequired,
hasGradesToPost: bool.isRequired, hasGradesOrPostableComments: bool.isRequired,
hasGradesOrCommentsToPost: bool.isRequired,
newIconsEnabled: bool.isRequired, newIconsEnabled: bool.isRequired,
onSelect: func.isRequired onSelect: func.isRequired
}).isRequired, }).isRequired,
@ -423,9 +424,7 @@ export default class AssignmentColumnHeader extends ColumnHeader {
{this.props.postGradesAction.featureEnabled ? ( {this.props.postGradesAction.featureEnabled ? (
<Menu.Item <Menu.Item
disabled={ disabled={!this.props.postGradesAction.hasGradesOrCommentsToPost}
!this.props.postGradesAction.hasGradesToPost || !this.props.postGradesAction.hasGrades
}
onSelect={this.postGrades} onSelect={this.postGrades}
> >
{labelForPostGradesAction(this.props.postGradesAction)} {labelForPostGradesAction(this.props.postGradesAction)}
@ -445,9 +444,7 @@ export default class AssignmentColumnHeader extends ColumnHeader {
{this.props.postGradesAction.featureEnabled && ( {this.props.postGradesAction.featureEnabled && (
<Menu.Item <Menu.Item
disabled={ disabled={!this.props.hideGradesAction.hasGradesOrCommentsToHide}
!this.props.hideGradesAction.hasGradesToHide || !this.props.hideGradesAction.hasGrades
}
onSelect={this.hideGrades} onSelect={this.hideGrades}
> >
{labelForHideGradesAction(this.props.hideGradesAction)} {labelForHideGradesAction(this.props.hideGradesAction)}
@ -507,7 +504,7 @@ export default class AssignmentColumnHeader extends ColumnHeader {
} }
const submissions = this.props.students.map(student => student.submission) const submissions = this.props.students.map(student => student.submission)
const postableSubmissionsPresent = submissions.some(isHidden) const postableSubmissionsPresent = submissions.some(isPostable)
if (newIconsEnabled) { if (newIconsEnabled) {
// Assignment has at least one hidden submission that can be posted // Assignment has at least one hidden submission that can be posted

View File

@ -18,7 +18,7 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import {isGraded, isHidden} from '../../../../grading/helpers/SubmissionHelper' import {isGraded, isPostable} from '../../../../grading/helpers/SubmissionHelper'
import {optionsForGradingType} from '../../../shared/EnterGradesAsSetting' import {optionsForGradingType} from '../../../shared/EnterGradesAsSetting'
import AssignmentColumnHeader from './AssignmentColumnHeader' import AssignmentColumnHeader from './AssignmentColumnHeader'
@ -26,11 +26,19 @@ function getSubmission(student, assignmentId) {
const submission = student[`assignment_${assignmentId}`] const submission = student[`assignment_${assignmentId}`]
if (!submission) { if (!submission) {
return {excused: false, latePolicyStatus: null, postedAt: null, score: null, submittedAt: null} return {
excused: false,
hasPostableComments: false,
latePolicyStatus: null,
postedAt: null,
score: null,
submittedAt: null
}
} }
return { return {
excused: submission.excused, excused: submission.excused,
hasPostableComments: submission.has_postable_comments,
latePolicyStatus: submission.late_policy_status, latePolicyStatus: submission.late_policy_status,
postedAt: submission.posted_at, postedAt: submission.posted_at,
score: submission.score, score: submission.score,
@ -62,7 +70,9 @@ function getProps(column, gradebook, options) {
submission: getSubmission(student, assignmentId) submission: getSubmission(student, assignmentId)
})) }))
const hasGrades = students.some(student => isGraded(student.submission)) const hasGradesOrPostableComments = students.some(
student => isGraded(student.submission) || student.submission.hasPostableComments
)
return { return {
ref: options.ref, ref: options.ref,
@ -104,8 +114,8 @@ function getProps(column, gradebook, options) {
}, },
hideGradesAction: { hideGradesAction: {
hasGrades, hasGradesOrPostableComments,
hasGradesToHide: students.some(student => student.submission.postedAt != null), hasGradesOrCommentsToHide: students.some(student => student.submission.postedAt != null),
onSelect(onExited) { onSelect(onExited) {
if (gradebook.postPolicies) { if (gradebook.postPolicies) {
gradebook.postPolicies.showHideAssignmentGradesTray({assignmentId, onExited}) gradebook.postPolicies.showHideAssignmentGradesTray({assignmentId, onExited})
@ -115,8 +125,8 @@ function getProps(column, gradebook, options) {
postGradesAction: { postGradesAction: {
featureEnabled: gradebook.postPolicies != null, featureEnabled: gradebook.postPolicies != null,
hasGrades, hasGradesOrPostableComments,
hasGradesToPost: students.some(student => isHidden(student.submission)), hasGradesOrCommentsToPost: students.some(student => isPostable(student.submission)),
newIconsEnabled: !!gradebook.options.new_post_policy_icons_enabled, newIconsEnabled: !!gradebook.options.new_post_policy_icons_enabled,
onSelect(onExited) { onSelect(onExited) {
if (gradebook.postPolicies) { if (gradebook.postPolicies) {

View File

@ -26,12 +26,14 @@ import PostAssignmentGradesTray from '../../../grading/PostAssignmentGradesTray'
function getSubmission(student, assignmentId) { function getSubmission(student, assignmentId) {
const submission = student[`assignment_${assignmentId}`] || { const submission = student[`assignment_${assignmentId}`] || {
has_postable_comments: false,
posted_at: null, posted_at: null,
score: null, score: null,
workflow_state: null workflow_state: null
} }
return { return {
hasPostableComments: !!submission.has_postable_comments,
postedAt: submission.posted_at, postedAt: submission.posted_at,
score: submission.score, score: submission.score,
workflowState: submission.workflow_state workflowState: submission.workflow_state

View File

@ -22,7 +22,7 @@ import I18n from 'i18n!gradezilla'
import {View} from '@instructure/ui-layout' import {View} from '@instructure/ui-layout'
import {Pill} from '@instructure/ui-elements' import {Pill} from '@instructure/ui-elements'
import Message from './SubmissionStatus/Message' import Message from './SubmissionStatus/Message'
import {isHidden} from '../../../grading/helpers/SubmissionHelper' import {isPostable} from '../../../grading/helpers/SubmissionHelper'
export default class SubmissionStatus extends React.Component { export default class SubmissionStatus extends React.Component {
static defaultProps = { static defaultProps = {
@ -46,6 +46,7 @@ export default class SubmissionStatus extends React.Component {
submission: shape({ submission: shape({
drop: bool, drop: bool,
excused: bool, excused: bool,
hasPostableComments: bool,
postedAt: instanceOf(Date), postedAt: instanceOf(Date),
score: number, score: number,
workflowState: string.isRequired workflowState: string.isRequired
@ -57,7 +58,7 @@ export default class SubmissionStatus extends React.Component {
const statusPillComponents = [] const statusPillComponents = []
if (postPoliciesEnabled) { if (postPoliciesEnabled) {
if (isHidden(submission)) { if (isPostable(submission)) {
statusPillComponents.push( statusPillComponents.push(
<Pill <Pill
key="hidden-submission" key="hidden-submission"

View File

@ -111,7 +111,8 @@ export default class SubmissionTray extends React.Component {
pointsDeducted: number, pointsDeducted: number,
postedAt: string.isRequired, postedAt: string.isRequired,
secondsLate: number.isRequired, secondsLate: number.isRequired,
assignmentId: string.isRequired assignmentId: string.isRequired,
hasPostableComments: bool.isRequired
}), }),
isFirstAssignment: bool.isRequired, isFirstAssignment: bool.isRequired,
isLastAssignment: bool.isRequired, isLastAssignment: bool.isRequired,

View File

@ -44,7 +44,9 @@ export default function PostTypes({anonymousGrading, defaultValue, disabled, pos
<> <>
<Text>{I18n.t('Everyone')}</Text> <Text>{I18n.t('Everyone')}</Text>
<br /> <br />
<Text size="small">{I18n.t('Grades will be made visible to all students')}</Text> <Text size="small">
{I18n.t('All students will be able to see their grade and/or submission comments.')}
</Text>
</> </>
} }
value={EVERYONE} value={EVERYONE}
@ -56,7 +58,9 @@ export default function PostTypes({anonymousGrading, defaultValue, disabled, pos
<Text>{I18n.t('Graded')}</Text> <Text>{I18n.t('Graded')}</Text>
<br /> <br />
<Text size="small"> <Text size="small">
{I18n.t('Grades will be made visible to students with graded submissions')} {I18n.t(
'Students who have received a grade or a submission comment will be able to see their grade and/or submission comments.'
)}
</Text> </Text>
</> </>
} }

View File

@ -30,7 +30,7 @@ import {
postAssignmentGradesForSections, postAssignmentGradesForSections,
resolvePostAssignmentGradesStatus resolvePostAssignmentGradesStatus
} from './Api' } from './Api'
import {isHidden} from '../helpers/SubmissionHelper' import {isPostable} from '../helpers/SubmissionHelper'
import {showFlashAlert} from '../../shared/FlashAlert' import {showFlashAlert} from '../../shared/FlashAlert'
function initialShowState() { function initialShowState() {
@ -175,7 +175,7 @@ export default class PostAssignmentGradesTray extends PureComponent {
return null return null
} }
const unpostedCount = submissions.filter(submission => isHidden(submission)).length const unpostedCount = submissions.filter(submission => isPostable(submission)).length
return ( return (
<Tray <Tray

View File

@ -24,9 +24,9 @@ export function isGraded(submission) {
return (sub.score != null && sub.workflowState === 'graded') || sub.excused return (sub.score != null && sub.workflowState === 'graded') || sub.excused
} }
export function isHidden(submission) { export function isPostable(submission) {
const sub = camelize(submission) const sub = camelize(submission)
return isGraded(sub) && !sub.postedAt return !sub.postedAt && (isGraded(sub) || !!sub.hasPostableComments)
} }
// This function returns an object containing plagiarism/originality-related // This function returns an object containing plagiarism/originality-related

View File

@ -114,6 +114,7 @@ export default class PostPolicies {
onPosted, onPosted,
sections: this._sections, sections: this._sections,
submissions: submissions.map(submission => ({ submissions: submissions.map(submission => ({
hasPostableComments: !!submission.has_postable_comments,
postedAt: submission.posted_at, postedAt: submission.posted_at,
score: submission.score, score: submission.score,
workflowState: submission.workflow_state workflowState: submission.workflow_state

View File

@ -25,7 +25,8 @@ import {Text} from '@instructure/ui-elements'
import I18n from 'i18n!SpeedGraderPostGradesMenu' import I18n from 'i18n!SpeedGraderPostGradesMenu'
export default function SpeedGraderPostGradesMenu(props) { export default function SpeedGraderPostGradesMenu(props) {
const Icon = props.allowPostingGrades ? IconOffLine : IconEyeLine const {allowHidingGradesOrComments, allowPostingGradesOrComments} = props
const Icon = allowPostingGradesOrComments ? IconOffLine : IconEyeLine
const menuTrigger = ( const menuTrigger = (
<Button <Button
icon={<Icon className="speedgrader-postgradesmenu-icon" />} icon={<Icon className="speedgrader-postgradesmenu-icon" />}
@ -36,23 +37,31 @@ export default function SpeedGraderPostGradesMenu(props) {
return ( return (
<Menu placement="bottom end" trigger={menuTrigger}> <Menu placement="bottom end" trigger={menuTrigger}>
{props.allowPostingGrades && props.hasGrades ? ( {allowPostingGradesOrComments ? (
<Menu.Item name="postGrades" onSelect={props.onPostGrades}> <Menu.Item name="postGrades" onSelect={props.onPostGrades}>
<Text>{I18n.t('Post Grades')}</Text> <Text>{I18n.t('Post Grades')}</Text>
</Menu.Item> </Menu.Item>
) : ( ) : (
<Menu.Item name="postGrades" disabled> <Menu.Item name="postGrades" disabled>
<Text>{props.hasGrades ? I18n.t('All Grades Posted') : I18n.t('No Grades to Post')}</Text> <Text>
{props.hasGradesOrPostableComments
? I18n.t('All Grades Posted')
: I18n.t('No Grades to Post')}
</Text>
</Menu.Item> </Menu.Item>
)} )}
{props.allowHidingGrades && props.hasGrades ? ( {allowHidingGradesOrComments ? (
<Menu.Item name="hideGrades" onSelect={props.onHideGrades}> <Menu.Item name="hideGrades" onSelect={props.onHideGrades}>
<Text>{I18n.t('Hide Grades')}</Text> <Text>{I18n.t('Hide Grades')}</Text>
</Menu.Item> </Menu.Item>
) : ( ) : (
<Menu.Item name="hideGrades" disabled> <Menu.Item name="hideGrades" disabled>
<Text>{props.hasGrades ? I18n.t('All Grades Hidden') : I18n.t('No Grades to Hide')}</Text> <Text>
{props.hasGradesOrPostableComments
? I18n.t('All Grades Hidden')
: I18n.t('No Grades to Hide')}
</Text>
</Menu.Item> </Menu.Item>
)} )}
</Menu> </Menu>
@ -60,9 +69,9 @@ export default function SpeedGraderPostGradesMenu(props) {
} }
SpeedGraderPostGradesMenu.propTypes = { SpeedGraderPostGradesMenu.propTypes = {
allowHidingGrades: bool.isRequired, allowHidingGradesOrComments: bool.isRequired,
allowPostingGrades: bool.isRequired, allowPostingGradesOrComments: bool.isRequired,
hasGrades: bool.isRequired, hasGradesOrPostableComments: bool.isRequired,
onHideGrades: func.isRequired, onHideGrades: func.isRequired,
onPostGrades: func.isRequired onPostGrades: func.isRequired
} }

View File

@ -177,6 +177,10 @@ module SpeedGrader
json.merge! provisional_grade_to_json(provisional_grade) json.merge! provisional_grade_to_json(provisional_grade)
end end
if course.root_account.feature_enabled?(:allow_postable_submission_comments) && course.post_policies_enabled?
json[:has_postable_comments] = sub.submission_comments.select(&:hidden?).present?
end
json[:submission_comments] = anonymous_moderated_submission_comments_json( json[:submission_comments] = anonymous_moderated_submission_comments_json(
assignment: assignment, assignment: assignment,
course: course, course: course,

View File

@ -143,6 +143,11 @@ class Submission < ActiveRecord::Base
scope :for_context_codes, lambda { |context_codes| where(:context_code => context_codes) } scope :for_context_codes, lambda { |context_codes| where(:context_code => context_codes) }
scope :postable, -> { graded.union(with_hidden_comments) }
scope :with_hidden_comments, -> {
where("EXISTS (?)", SubmissionComment.where("submission_id = submissions.id AND hidden = true"))
}
# This should only be used in the course drop down to show assignments recently graded. # This should only be used in the course drop down to show assignments recently graded.
scope :recently_graded_assignments, lambda { |user_id, date, limit| scope :recently_graded_assignments, lambda { |user_id, date, limit|
select("assignments.id, assignments.title, assignments.points_possible, assignments.due_at, select("assignments.id, assignments.title, assignments.points_possible, assignments.due_at,

View File

@ -17,3 +17,10 @@ add_grading_scheme_to_admin_grade_reports:
display_name: Add Grading Scheme to Admin Grade Reports display_name: Add Grading Scheme to Admin Grade Reports
description: Includes grade values in the admin grade reports. description: Includes grade values in the admin grade reports.
applies_to: RootAccount applies_to: RootAccount
allow_postable_submission_comments:
state: hidden
display_name: Allow Postable Submission Comments
description: For manually posted assignments, the presence of a submission comment not by the submission's own
user will allow posting the assignment. Previously, this would have required a grade before posting could
occur.
applies_to: RootAccount

View File

@ -70,6 +70,10 @@ module Api::V1::Submission
) )
end end
if includes.include?("has_postable_comments")
hash["has_postable_comments"] = submission.submission_comments.select(&:hidden?).present?
end
if includes.include?("submission_comments") if includes.include?("submission_comments")
published_comments = submission.comments_for(@current_user).published published_comments = submission.comments_for(@current_user).published
hash['submission_comments'] = submission_comments_json(published_comments, current_user) hash['submission_comments'] = submission_comments_json(published_comments, current_user)

View File

@ -36,7 +36,7 @@ import PostPolicies from 'jsx/speed_grader/PostPolicies'
import SpeedGraderProvisionalGradeSelector from 'jsx/speed_grader/SpeedGraderProvisionalGradeSelector' import SpeedGraderProvisionalGradeSelector from 'jsx/speed_grader/SpeedGraderProvisionalGradeSelector'
import SpeedGraderPostGradesMenu from 'jsx/speed_grader/SpeedGraderPostGradesMenu' import SpeedGraderPostGradesMenu from 'jsx/speed_grader/SpeedGraderPostGradesMenu'
import SpeedGraderSettingsMenu from 'jsx/speed_grader/SpeedGraderSettingsMenu' import SpeedGraderSettingsMenu from 'jsx/speed_grader/SpeedGraderSettingsMenu'
import {isGraded, isHidden} from 'jsx/grading/helpers/SubmissionHelper' import {isGraded, isPostable} from 'jsx/grading/helpers/SubmissionHelper'
import studentViewedAtTemplate from 'jst/speed_grader/student_viewed_at' import studentViewedAtTemplate from 'jst/speed_grader/student_viewed_at'
import submissionsDropdownTemplate from 'jst/speed_grader/submissions_dropdown' import submissionsDropdownTemplate from 'jst/speed_grader/submissions_dropdown'
import speechRecognitionTemplate from 'jst/speed_grader/speech_recognition' import speechRecognitionTemplate from 'jst/speed_grader/speech_recognition'
@ -758,7 +758,7 @@ function renderProgressIcon(attachment) {
function renderHiddenSubmissionPill(submission) { function renderHiddenSubmissionPill(submission) {
const mountPoint = document.getElementById(SPEED_GRADER_HIDDEN_SUBMISSION_PILL_MOUNT_POINT) const mountPoint = document.getElementById(SPEED_GRADER_HIDDEN_SUBMISSION_PILL_MOUNT_POINT)
if (isHidden(submission)) { if (isPostable(submission)) {
ReactDOM.render( ReactDOM.render(
<Pill variant="warning" text={I18n.t('Hidden')} margin="0 0 small" />, <Pill variant="warning" text={I18n.t('Hidden')} margin="0 0 small" />,
mountPoint mountPoint
@ -3752,11 +3752,15 @@ function renderPostGradesMenu() {
const {submissionsMap} = window.jsonData const {submissionsMap} = window.jsonData
const submissions = window.jsonData.studentsWithSubmissions.map(student => student.submission) const submissions = window.jsonData.studentsWithSubmissions.map(student => student.submission)
const hasGrades = submissions.some(isGraded) const hasGradesOrPostableComments = submissions.some(
const allowHidingGrades = submissions.some( submission => isGraded(submission) || submission.has_postable_comments
)
const allowHidingGradesOrComments = submissions.some(
submission => submission && submission.posted_at != null submission => submission && submission.posted_at != null
) )
const allowPostingGrades = submissions.some(submission => submission && isHidden(submission)) const allowPostingGradesOrComments = submissions.some(
submission => submission && isPostable(submission)
)
function onHideGrades() { function onHideGrades() {
EG.postPolicies.showHideAssignmentGradesTray({submissionsMap}) EG.postPolicies.showHideAssignmentGradesTray({submissionsMap})
@ -3767,9 +3771,9 @@ function renderPostGradesMenu() {
} }
const props = { const props = {
allowHidingGrades, allowHidingGradesOrComments,
allowPostingGrades, allowPostingGradesOrComments,
hasGrades, hasGradesOrPostableComments,
onHideGrades, onHideGrades,
onPostGrades onPostGrades
} }

View File

@ -1689,6 +1689,59 @@ describe 'Submissions API', type: :request do
expect(response).to be_forbidden expect(response).to be_forbidden
end end
describe "has_postable_comments" do
let(:assignment) { @course.assignments.create! }
let(:student1_sub) { assignment.submissions.find_by(user: @student1) }
before(:each) do
@course.root_account.enable_feature!(:allow_postable_submission_comments)
PostPolicy.enable_feature!
@course.enable_feature!(:new_gradebook)
assignment.ensure_post_policy(post_manually: true)
end
def student_json(params = {grouped: true, student_ids: [@student1.to_param]})
api_call(
:get,
"/api/v1/courses/#{@course.id}/students/submissions.json",
{
controller: "submissions_api",
action: "for_students",
format: "json",
course_id: @course.to_param
},
params
).first
end
it "is not included when allow_postable_submission_comments feature is not enabled" do
@course.root_account.disable_feature!(:allow_postable_submission_comments)
expect(student_json.fetch("submissions").first).not_to have_key "has_postable_comments"
end
it "is not included when Post Policies are not enabled" do
@course.disable_feature!(:new_gradebook)
expect(student_json.fetch("submissions").first).not_to have_key "has_postable_comments"
end
it "is not included when params[:grouped] is not present" do
submission_json = student_json({student_ids: [@student1.to_param]})
expect(submission_json).not_to have_key "has_postable_comments"
end
it "is true when unposted and hidden comments exist" do
student1_sub.add_comment(author: @teacher, comment: "good job!", hidden: true)
submission_json = student_json.fetch("submissions").find { |s| s.fetch("id") == student1_sub.id }
expect(submission_json.fetch("has_postable_comments")).to be true
end
it "is false when unposted and only non-hidden comments exist" do
student1_sub.add_comment(author: @student, comment: "fun assignment!", hidden: false)
submission_json = student_json.fetch("submissions").find { |s| s.fetch("id") == student1_sub.id }
expect(submission_json.fetch("has_postable_comments")).to be false
end
end
context 'OriginalityReport' do context 'OriginalityReport' do
it 'includes has_originality_report if the submission has an originality_report' do it 'includes has_originality_report if the submission has an originality_report' do
attachment_model attachment_model

View File

@ -206,41 +206,62 @@ describe Mutations::PostAssignmentGradesForSections do
end end
describe "graded_only" do describe "graded_only" do
let(:post_submissions_job) { Delayed::Job.where(tag: "Assignment#post_submissions").order(:id).last }
let(:section1_user_ids) { section1.enrollments.pluck(:user_id) } let(:section1_user_ids) { section1.enrollments.pluck(:user_id) }
let(:section1_submissions) { assignment.submissions.where(user_id: section1_user_ids) } let(:section1_submissions) { assignment.submissions.where(user_id: section1_user_ids) }
before(:each) do before(:each) do
section1_student2 = User.create! @section1_student2 = User.create!
section1.enroll_user(section1_student2, "StudentEnrollment", "active") section1.enroll_user(@section1_student2, "StudentEnrollment", "active")
@student1_submission = assignment.submissions.find_by(user: @section1_student) @student1_submission = assignment.submissions.find_by(user: @section1_student)
@student2_submission = assignment.submissions.find_by(user: section1_student2) @student2_submission = assignment.submissions.find_by(user: @section1_student2)
assignment.ensure_post_policy(post_manually: true)
assignment.grade_student(@section1_student, grader: teacher, score: 100) assignment.grade_student(@section1_student, grader: teacher, score: 100)
end end
it "posts the graded submissions if graded_only is true" do it "posts the graded submissions if graded_only is true" do
execute_query(mutation_str(assignment_id: assignment.id, section_ids:[section1.id], graded_only: true), context) execute_query(mutation_str(assignment_id: assignment.id, section_ids:[section1.id], graded_only: true), context)
post_submissions_job = Delayed::Job.where(tag: "Assignment#post_submissions").order(:id).last
post_submissions_job.invoke_job post_submissions_job.invoke_job
expect(@student1_submission.reload).to be_posted expect(@student1_submission.reload).to be_posted
end end
it "posts submissions with hidden comments if graded_only is true and post comments feature is enabled" do
course.root_account.enable_feature!(:allow_postable_submission_comments)
@student2_submission.add_comment(author: teacher, comment: "good work!", hidden: true)
execute_query(mutation_str(assignment_id: assignment.id, section_ids:[section1.id], graded_only: true), context)
post_submissions_job.invoke_job
expect(@student2_submission.reload).to be_posted
end
it "does not post submissions with hidden comments if graded_only is true and post comments feature is not enabled" do
@student2_submission.add_comment(author: teacher, comment: "good work!", hidden: true)
execute_query(mutation_str(assignment_id: assignment.id, section_ids:[section1.id], graded_only: true), context)
post_submissions_job.invoke_job
expect(@student2_submission.reload).not_to be_posted
end
it "does not post submissions with no hidden comments if graded_only is true and post comments feature is enabled" do
course.root_account.enable_feature!(:allow_postable_submission_comments)
@student2_submission.add_comment(author: @section1_student2, comment: "good work!", hidden: false)
execute_query(mutation_str(assignment_id: assignment.id, section_ids:[section1.id], graded_only: true), context)
post_submissions_job.invoke_job
expect(@student2_submission.reload).not_to be_posted
end
it "does not post the ungraded submissions if graded_only is true" do it "does not post the ungraded submissions if graded_only is true" do
execute_query(mutation_str(assignment_id: assignment.id, section_ids:[section1.id], graded_only: true), context) execute_query(mutation_str(assignment_id: assignment.id, section_ids:[section1.id], graded_only: true), context)
post_submissions_job = Delayed::Job.where(tag: "Assignment#post_submissions").order(:id).last
post_submissions_job.invoke_job post_submissions_job.invoke_job
expect(@student2_submission.reload).not_to be_posted expect(@student2_submission.reload).not_to be_posted
end end
it "posts all the submissions if graded_only is false" do it "posts all the submissions if graded_only is false" do
execute_query(mutation_str(assignment_id: assignment.id, section_ids:[section1.id], graded_only: false), context) execute_query(mutation_str(assignment_id: assignment.id, section_ids:[section1.id], graded_only: false), context)
post_submissions_job = Delayed::Job.where(tag: "Assignment#post_submissions").order(:id).last
post_submissions_job.invoke_job post_submissions_job.invoke_job
expect(section1_submissions).to all(be_posted) expect(section1_submissions).to all(be_posted)
end end
it "posts all the sections' submissions if graded_only is not present" do it "posts all the sections' submissions if graded_only is not present" do
execute_query(mutation_str(assignment_id: assignment.id, section_ids:[section1.id]), context) execute_query(mutation_str(assignment_id: assignment.id, section_ids:[section1.id]), context)
post_submissions_job = Delayed::Job.where(tag: "Assignment#post_submissions").order(:id).last
post_submissions_job.invoke_job post_submissions_job.invoke_job
expect(section1_submissions).to all(be_posted) expect(section1_submissions).to all(be_posted)
end end

View File

@ -277,6 +277,7 @@ describe Mutations::PostAssignmentGrades do
before(:each) do before(:each) do
@student1_submission = assignment.submissions.find_by(user: student) @student1_submission = assignment.submissions.find_by(user: student)
@student2_submission = assignment.submissions.find_by(user: student2) @student2_submission = assignment.submissions.find_by(user: student2)
assignment.ensure_post_policy(post_manually: true)
assignment.grade_student(student, grader: teacher, score: 100) assignment.grade_student(student, grader: teacher, score: 100)
end end
@ -293,6 +294,29 @@ describe Mutations::PostAssignmentGrades do
expect(@student1_submission.reload).to be_posted expect(@student1_submission.reload).to be_posted
end end
it "posts submissions with hidden comments if graded_only is true and post comments feature is enabled" do
course.root_account.enable_feature!(:allow_postable_submission_comments)
@student2_submission.add_comment(author: teacher, comment: "good work!", hidden: true)
execute_query(mutation_str(assignment_id: assignment.id, graded_only: true), context)
post_submissions_job.invoke_job
expect(@student2_submission.reload).to be_posted
end
it "does not post submissions with hidden comments if graded_only is true and post comments feature is not enabled" do
@student2_submission.add_comment(author: teacher, comment: "good work!", hidden: true)
execute_query(mutation_str(assignment_id: assignment.id, graded_only: true), context)
post_submissions_job.invoke_job
expect(@student2_submission.reload).not_to be_posted
end
it "does not post submissions with no hidden comments if graded_only is true and post comments feature is enabled" do
course.root_account.enable_feature!(:allow_postable_submission_comments)
@student2_submission.add_comment(author: student, comment: "good work!", hidden: false)
execute_query(mutation_str(assignment_id: assignment.id, graded_only: true), context)
post_submissions_job.invoke_job
expect(@student2_submission.reload).not_to be_posted
end
it "does not post the ungraded submissions if graded_only is true" do it "does not post the ungraded submissions if graded_only is true" do
execute_query(mutation_str(assignment_id: assignment.id, graded_only: true), context) execute_query(mutation_str(assignment_id: assignment.id, graded_only: true), context)
post_submissions_job.invoke_job post_submissions_job.invoke_job

View File

@ -375,10 +375,9 @@ QUnit.module('GradebookGrid AssignmentCellFormatter', suiteHooks => {
strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 1) strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 1)
}) })
test('does not display an unposted grade indicator when submission is graded and posted', () => { test('displays an unposted grade indicator when a submission comment exists and is unposted', () => {
submission.workflow_state = 'graded' submission.hasPostableComments = true
submission.posted_at = new Date() strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 1)
strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 0)
}) })
test('does not display an unposted grade indicator when grade is posted', () => { test('does not display an unposted grade indicator when grade is posted', () => {
@ -386,14 +385,15 @@ QUnit.module('GradebookGrid AssignmentCellFormatter', suiteHooks => {
strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 0) strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 0)
}) })
test('does not display an unposted grade indicator when submission not graded', () => { test('does not display an unposted grade indicator when submission has no grade nor comment', () => {
submission.workflow_state = 'unsubmitted' submission.workflow_state = 'unsubmitted'
strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 0) strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 0)
}) })
test('does not display an unposted grade indicator when submission does not have a score', () => { test('does not display an unposted grade indicator when submission does not have a score nor postable comment', () => {
submission.workflow_state = 'graded' submission.workflow_state = 'graded'
submission.score = null submission.score = null
submission.hasPostableComments = false
strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 0) strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 0)
}) })
}) })

View File

@ -83,6 +83,7 @@ QUnit.module('GradebookGrid AssignmentColumnHeaderRenderer', suiteHooks => {
id: '93', id: '93',
assignment_id: '2301', assignment_id: '2301',
excused: false, excused: false,
hasPostableComments: false,
late_policy_status: null, late_policy_status: null,
posted_at: null, posted_at: null,
score: null, score: null,
@ -362,34 +363,41 @@ QUnit.module('GradebookGrid AssignmentColumnHeaderRenderer', suiteHooks => {
strictEqual(component.props.postGradesAction.featureEnabled, true) strictEqual(component.props.postGradesAction.featureEnabled, true)
}) })
test('sets hasGrades to true if at least one graded submission is graded', () => { test('sets hasGradesOrPostableComments to true if at least one submission is graded', () => {
submission.workflow_state = 'graded' submission.workflow_state = 'graded'
submission.score = 1 submission.score = 1
gradebook.gotChunkOfStudents([student]) gradebook.gotChunkOfStudents([student])
render() render()
strictEqual(component.props.postGradesAction.hasGrades, true) strictEqual(component.props.postGradesAction.hasGradesOrPostableComments, true)
}) })
test('sets hasGrades to false if no submissions are graded', () => { test('sets hasGradesOrPostableComments to false if no submissions are graded', () => {
gradebook.gotChunkOfStudents([student]) gradebook.gotChunkOfStudents([student])
render() render()
strictEqual(component.props.postGradesAction.hasGrades, false) strictEqual(component.props.postGradesAction.hasGradesOrPostableComments, false)
}) })
test('sets hasGradesToPost to true if at least one graded submission has no posted_at date', () => { test('sets hasGradesOrCommentsToPost to true if at least one submission is graded and unposted', () => {
submission.workflow_state = 'graded' submission.workflow_state = 'graded'
submission.score = 1 submission.score = 1
gradebook.gotChunkOfStudents([student]) gradebook.gotChunkOfStudents([student])
render() render()
strictEqual(component.props.postGradesAction.hasGradesToPost, true) strictEqual(component.props.postGradesAction.hasGradesOrCommentsToPost, true)
}) })
test('sets hasGradesToPost to false if all submissions have a posted_at date', () => { test('sets hasGradesOrCommentsToPost to true if at least one submission has a postable comment and unposted', () => {
submission.has_postable_comments = true
gradebook.gotChunkOfStudents([student])
render()
strictEqual(component.props.postGradesAction.hasGradesOrCommentsToPost, true)
})
test('sets hasGradesOrCommentsToPost to false if all submissions have a posted_at date', () => {
submission.posted_at = new Date('Wed Oct 1 1997') submission.posted_at = new Date('Wed Oct 1 1997')
gradebook.gotChunkOfStudents([student]) gradebook.gotChunkOfStudents([student])
render() render()
strictEqual(component.props.postGradesAction.hasGradesToPost, false) strictEqual(component.props.postGradesAction.hasGradesOrCommentsToPost, false)
}) })
test('sets newIconsEnabled to true if Gradebook has new_post_policy_icons_enabled set to true', () => { test('sets newIconsEnabled to true if Gradebook has new_post_policy_icons_enabled set to true', () => {
@ -447,31 +455,38 @@ QUnit.module('GradebookGrid AssignmentColumnHeaderRenderer', suiteHooks => {
sinon.stub(gradebook.postPolicies, 'showHideAssignmentGradesTray') sinon.stub(gradebook.postPolicies, 'showHideAssignmentGradesTray')
}) })
test('sets hasGrades to true if at least one graded submission is graded', () => { test('sets hasGradesOrPostableComments to true if at least one submission is graded', () => {
submission.workflow_state = 'graded' submission.workflow_state = 'graded'
submission.score = 1 submission.score = 1
gradebook.gotChunkOfStudents([student]) gradebook.gotChunkOfStudents([student])
render() render()
strictEqual(component.props.hideGradesAction.hasGrades, true) strictEqual(component.props.hideGradesAction.hasGradesOrPostableComments, true)
}) })
test('sets hasGrades to false if no submissions are graded', () => { test('sets hasGradesOrPostableComments to true if at least one submission has postable comments', () => {
submission.has_postable_comments = true
gradebook.gotChunkOfStudents([student]) gradebook.gotChunkOfStudents([student])
render() render()
strictEqual(component.props.hideGradesAction.hasGrades, false) strictEqual(component.props.hideGradesAction.hasGradesOrPostableComments, true)
}) })
test('sets hasGradesToHide to true if at least one submission has a posted_at date', () => { test('sets hasGradesOrPostableComments to false if no submissions are graded or have comments', () => {
gradebook.gotChunkOfStudents([student]) gradebook.gotChunkOfStudents([student])
render() render()
strictEqual(component.props.hideGradesAction.hasGradesToHide, true) strictEqual(component.props.hideGradesAction.hasGradesOrPostableComments, false)
}) })
test('sets hasGradesToHide to false if no submission has a posted_at date', () => { test('sets hasGradesOrCommentsToHide to true if at least one submission has a posted_at date', () => {
gradebook.gotChunkOfStudents([student])
render()
strictEqual(component.props.hideGradesAction.hasGradesOrCommentsToHide, true)
})
test('sets hasGradesOrCommentsToHide to false if no submission has a posted_at date', () => {
submission.posted_at = null submission.posted_at = null
gradebook.gotChunkOfStudents([student]) gradebook.gotChunkOfStudents([student])
render() render()
strictEqual(component.props.hideGradesAction.hasGradesToHide, false) strictEqual(component.props.hideGradesAction.hasGradesOrCommentsToHide, false)
}) })
test('includes a callback to show the "Hide Assignment Grades" tray', () => { test('includes a callback to show the "Hide Assignment Grades" tray', () => {

View File

@ -77,8 +77,8 @@ QUnit.module('GradebookGrid AssignmentColumnHeader', suiteHooks => {
}, },
hideGradesAction: { hideGradesAction: {
hasGrades: true, hasGradesOrPostableComments: true,
hasGradesToHide: true, hasGradesOrCommentsToHide: true,
onSelect() {} onSelect() {}
}, },
@ -90,8 +90,8 @@ QUnit.module('GradebookGrid AssignmentColumnHeader', suiteHooks => {
postGradesAction: { postGradesAction: {
enabled: false, enabled: false,
featureEnabled: false, featureEnabled: false,
hasGrades: true, hasGradesOrPostableComments: true,
hasGradesToPost: true, hasGradesOrCommentsToPost: true,
onSelect() {} onSelect() {}
}, },
@ -1100,7 +1100,7 @@ QUnit.module('GradebookGrid AssignmentColumnHeader', suiteHooks => {
QUnit.module('"Options" > "Post grades" action', hooks => { QUnit.module('"Options" > "Post grades" action', hooks => {
hooks.beforeEach(() => { hooks.beforeEach(() => {
props.postGradesAction.featureEnabled = true props.postGradesAction.featureEnabled = true
props.postGradesAction.hasGradesToPost = true props.postGradesAction.hasGradesOrCommentsToPost = true
}) })
QUnit.module('when post policies is enabled', () => { QUnit.module('when post policies is enabled', () => {
@ -1115,34 +1115,26 @@ QUnit.module('GradebookGrid AssignmentColumnHeader', suiteHooks => {
}) })
test('has the text "All grades posted" when no submissions can be posted', () => { test('has the text "All grades posted" when no submissions can be posted', () => {
props.postGradesAction.hasGradesToPost = false props.postGradesAction.hasGradesOrCommentsToPost = false
mountAndOpenOptionsMenu() mountAndOpenOptionsMenu()
ok(getMenuItem($menuContent, 'All grades posted')) ok(getMenuItem($menuContent, 'All grades posted'))
}) })
test('has the text "No grades to post" when no submissions are graded', () => { test('has the text "No grades to post" when no submissions are graded or have comments', () => {
props.postGradesAction.hasGrades = false props.postGradesAction.hasGradesOrCommentsToPost = false
props.postGradesAction.hasGradesOrPostableComments = false
mountAndOpenOptionsMenu() mountAndOpenOptionsMenu()
ok(getMenuItem($menuContent, 'No grades to post')) ok(getMenuItem($menuContent, 'No grades to post'))
}) })
test('is disabled when no submissions can be posted', () => { test('is disabled when no submissions can be posted', () => {
props.postGradesAction.hasGradesToPost = false props.postGradesAction.hasGradesOrCommentsToPost = false
mountAndOpenOptionsMenu() mountAndOpenOptionsMenu()
strictEqual( strictEqual(
getMenuItem($menuContent, 'All grades posted').getAttribute('aria-disabled'), getMenuItem($menuContent, 'All grades posted').getAttribute('aria-disabled'),
'true' 'true'
) )
}) })
test('is disabled when no submissions are graded', () => {
props.postGradesAction.hasGrades = false
mountAndOpenOptionsMenu()
strictEqual(
getMenuItem($menuContent, 'No grades to post').getAttribute('aria-disabled'),
'true'
)
})
}) })
test('is not present when post policies is not enabled', () => { test('is not present when post policies is not enabled', () => {
@ -1181,7 +1173,7 @@ QUnit.module('GradebookGrid AssignmentColumnHeader', suiteHooks => {
QUnit.module('"Options" > "Hide grades" action', hooks => { QUnit.module('"Options" > "Hide grades" action', hooks => {
hooks.beforeEach(() => { hooks.beforeEach(() => {
props.postGradesAction.featureEnabled = true props.postGradesAction.featureEnabled = true
props.hideGradesAction.hasGradesToHide = true props.hideGradesAction.hasGradesOrCommentsToHide = true
}) })
QUnit.module('when post policies is enabled', () => { QUnit.module('when post policies is enabled', () => {
@ -1196,34 +1188,26 @@ QUnit.module('GradebookGrid AssignmentColumnHeader', suiteHooks => {
}) })
test('has the text "All grades hidden" when no submissions can be hidden', () => { test('has the text "All grades hidden" when no submissions can be hidden', () => {
props.hideGradesAction.hasGradesToHide = false props.hideGradesAction.hasGradesOrCommentsToHide = false
mountAndOpenOptionsMenu() mountAndOpenOptionsMenu()
ok(getMenuItem($menuContent, 'All grades hidden')) ok(getMenuItem($menuContent, 'All grades hidden'))
}) })
test('has the text "No grades to hide" when no submissions are graded', () => { test('has the text "No grades to hide" when no submissions are graded or have comments', () => {
props.hideGradesAction.hasGrades = false props.hideGradesAction.hasGradesOrCommentsToHide = false
props.hideGradesAction.hasGradesOrPostableComments = false
mountAndOpenOptionsMenu() mountAndOpenOptionsMenu()
ok(getMenuItem($menuContent, 'No grades to hide')) ok(getMenuItem($menuContent, 'No grades to hide'))
}) })
test('is disabled when no submissions can be hidden', () => { test('is disabled when no submissions can be hidden', () => {
props.hideGradesAction.hasGradesToHide = false props.hideGradesAction.hasGradesOrCommentsToHide = false
mountAndOpenOptionsMenu() mountAndOpenOptionsMenu()
strictEqual( strictEqual(
getMenuItem($menuContent, 'All grades hidden').getAttribute('aria-disabled'), getMenuItem($menuContent, 'All grades hidden').getAttribute('aria-disabled'),
'true' 'true'
) )
}) })
test('is disabled when no submissions are graded', () => {
props.hideGradesAction.hasGrades = false
mountAndOpenOptionsMenu()
strictEqual(
getMenuItem($menuContent, 'No grades to hide').getAttribute('aria-disabled'),
'true'
)
})
}) })
test('is present when post policies is enabled', () => { test('is present when post policies is enabled', () => {

View File

@ -253,6 +253,7 @@ QUnit.module('Gradebook PostPolicies', suiteHooks => {
} }
submission = { submission = {
assignment_id: '2301', assignment_id: '2301',
has_postable_comments: true,
posted_at: new Date().toISOString(), posted_at: new Date().toISOString(),
score: 1.0, score: 1.0,
workflow_state: 'graded' workflow_state: 'graded'
@ -309,7 +310,12 @@ QUnit.module('Gradebook PostPolicies', suiteHooks => {
postPolicies.showPostAssignmentGradesTray({assignmentId: '2301'}) postPolicies.showPostAssignmentGradesTray({assignmentId: '2301'})
const [{submissions}] = postPolicies._postAssignmentGradesTray.show.lastCall.args const [{submissions}] = postPolicies._postAssignmentGradesTray.show.lastCall.args
deepEqual(submissions, [ deepEqual(submissions, [
{postedAt: submission.posted_at, score: 1.0, workflowState: 'graded'} {
hasPostableComments: true,
postedAt: submission.posted_at,
score: 1.0,
workflowState: 'graded'
}
]) ])
}) })

View File

@ -40,6 +40,7 @@ QUnit.module('SubmissionStatus - Pills', hooks => {
submission: { submission: {
assignmentId: '1', assignmentId: '1',
excused: false, excused: false,
hasPostableComments: false,
late: false, late: false,
missing: false, missing: false,
postedAt: null, postedAt: null,
@ -176,6 +177,13 @@ QUnit.module('SubmissionStatus - Pills', hooks => {
strictEqual(hiddenPills.length, 1) strictEqual(hiddenPills.length, 1)
}) })
test('shows the "Hidden" pill when the submission has comments and not posted', () => {
props.submission.hasPostableComments = true
wrapper = mountComponent()
const hiddenPills = getHiddenPills()
strictEqual(hiddenPills.length, 1)
})
test('does not show the "Hidden" pill when the submission is not graded', () => { test('does not show the "Hidden" pill when the submission is not graded', () => {
props.submission.workflowState = 'unsubmitted' props.submission.workflowState = 'unsubmitted'
wrapper = mountComponent() wrapper = mountComponent()
@ -218,6 +226,7 @@ QUnit.module('SubmissionStatus - Grading Period not in any grading period warnin
postPoliciesEnabled: false, postPoliciesEnabled: false,
submission: { submission: {
excused: false, excused: false,
hasPostableComments: false,
late: false, late: false,
missing: false, missing: false,
secondsLate: 0, secondsLate: 0,
@ -278,6 +287,7 @@ QUnit.module('SubmissionStatus - Grading Period is a closed warning', hooks => {
postPoliciesEnabled: false, postPoliciesEnabled: false,
submission: { submission: {
excused: false, excused: false,
hasPostableComments: false,
late: false, late: false,
missing: false, missing: false,
secondsLate: 0, secondsLate: 0,
@ -338,6 +348,7 @@ QUnit.module('SubmissionStatus - Grading Period is in another period warning', h
postPoliciesEnabled: false, postPoliciesEnabled: false,
submission: { submission: {
excused: false, excused: false,
hasPostableComments: false,
late: false, late: false,
missing: false, missing: false,
secondsLate: 0, secondsLate: 0,
@ -398,6 +409,7 @@ QUnit.module('SubmissionStatus - Concluded Enrollment Warning', hooks => {
postPoliciesEnabled: false, postPoliciesEnabled: false,
submission: { submission: {
excused: false, excused: false,
hasPostableComments: false,
late: false, late: false,
missing: false, missing: false,
secondsLate: 0, secondsLate: 0,
@ -458,6 +470,7 @@ QUnit.module('SubmissionStatus - Not calculated in final grade', hooks => {
postPoliciesEnabled: false, postPoliciesEnabled: false,
submission: { submission: {
excused: false, excused: false,
hasPostableComments: false,
late: false, late: false,
missing: false, missing: false,
secondsLate: 0, secondsLate: 0,

View File

@ -74,7 +74,8 @@ QUnit.module('SubmissionTray', hooks => {
enteredScore: 10, enteredScore: 10,
excused: false, excused: false,
grade: '7', grade: '7',
gradedAt: null, gradedAt: new Date().toISOString(),
hasPostableComments: false,
id: '2501', id: '2501',
late: false, late: false,
missing: false, missing: false,
@ -83,7 +84,8 @@ QUnit.module('SubmissionTray', hooks => {
score: 7, score: 7,
secondsLate: 0, secondsLate: 0,
submissionType: 'online_text_entry', submissionType: 'online_text_entry',
userId: '27' userId: '27',
workflowState: 'graded'
}, },
updateSubmission() {}, updateSubmission() {},
updateSubmissionComment() {}, updateSubmissionComment() {},
@ -94,6 +96,7 @@ QUnit.module('SubmissionTray', hooks => {
gradingType: 'points', gradingType: 'points',
htmlUrl: 'http://htmlUrl/', htmlUrl: 'http://htmlUrl/',
id: '30', id: '30',
moderatedGrading: false,
muted: false, muted: false,
pointsPossible: 10, pointsPossible: 10,
postManually: false, postManually: false,
@ -294,6 +297,24 @@ QUnit.module('SubmissionTray', hooks => {
}) })
}) })
QUnit.module('when passing true for postPoliciesEnabled', contextHooks => {
contextHooks.beforeEach(() => {
defaultProps.postPoliciesEnabled = true
})
test('"Hidden" is displayed when a submission is graded and unposted', () => {
defaultProps.submission.workflowState = 'graded'
mountComponent()
ok(content.textContent.includes('Hidden'))
})
test('"Hidden" is displayed when a submission has comments and is unposted', () => {
defaultProps.submission.hasPostableComments = true
mountComponent()
ok(content.textContent.includes('Hidden'))
})
})
test('shows avatar if avatar is not null', () => { test('shows avatar if avatar is not null', () => {
const avatarUrl = 'http://bob_is_not_a_domain/me.jpg?filter=make_me_pretty' const avatarUrl = 'http://bob_is_not_a_domain/me.jpg?filter=make_me_pretty'
const gradesUrl = 'http://gradesUrl/' const gradesUrl = 'http://gradesUrl/'
@ -334,13 +355,6 @@ QUnit.module('SubmissionTray', hooks => {
ok(content.textContent.includes('This submission is not in any grading period')) ok(content.textContent.includes('This submission is not in any grading period'))
}) })
test('passes along postPoliciesEnabled prop to SubmissionStatus', () => {
defaultProps.postPoliciesEnabled = true
defaultProps.submission.workflowState = 'graded'
mountComponent()
ok(content.textContent.includes('Hidden'))
})
test('shows student name', () => { test('shows student name', () => {
mountComponent({ mountComponent({
student: {id: '27', name: 'Sara', gradesUrl: 'http://gradeUrl/', isConcluded: false} student: {id: '27', name: 'Sara', gradesUrl: 'http://gradeUrl/', isConcluded: false}

View File

@ -243,6 +243,16 @@ QUnit.module('PostAssignmentGradesTray', suiteHooks => {
await show() await show()
strictEqual(getUnpostedCount().textContent, '1') strictEqual(getUnpostedCount().textContent, '1')
}) })
test('submissions with postable comments and without a postedAt are counted', async () => {
context.submissions = [
{postedAt: new Date().toISOString(), hasPostableComments: true},
{postedAt: null, score: 1, workflowState: 'graded'},
{postedAt: null, score: null, workflowState: 'unsubmitted'}
]
await show()
strictEqual(getUnpostedCount().textContent, '1')
})
}) })
QUnit.module('with no unposted submissions', unpostedSubmissionsHooks => { QUnit.module('with no unposted submissions', unpostedSubmissionsHooks => {

View File

@ -30,12 +30,14 @@ QUnit.module('PostAssignmentGradesTray PostTypes', suiteHooks => {
} }
function getGradedPostType() { function getGradedPostType() {
const labelText = 'GradedGrades will be made visible to students with graded submissions' const labelText =
'GradedStudents who have received a grade or a submission comment will be able to see their grade and/or submission comments.'
return document.getElementById(getLabel(labelText).htmlFor) return document.getElementById(getLabel(labelText).htmlFor)
} }
function getEveryonePostType() { function getEveryonePostType() {
const labelText = 'EveryoneGrades will be made visible to all students' const labelText =
'EveryoneAll students will be able to see their grade and/or submission comments.'
return document.getElementById(getLabel(labelText).htmlFor) return document.getElementById(getLabel(labelText).htmlFor)
} }
@ -66,13 +68,15 @@ QUnit.module('PostAssignmentGradesTray PostTypes', suiteHooks => {
test('"Everyone" type includes description"', () => { test('"Everyone" type includes description"', () => {
mountComponent() mountComponent()
const labelText = 'EveryoneGrades will be made visible to all students' const labelText =
'EveryoneAll students will be able to see their grade and/or submission comments.'
ok(getLabel(labelText)) ok(getLabel(labelText))
}) })
test('"Graded" type includes description"', () => { test('"Graded" type includes description"', () => {
mountComponent() mountComponent()
const labelText = 'GradedGrades will be made visible to students with graded submissions' const labelText =
'GradedStudents who have received a grade or a submission comment will be able to see their grade and/or submission comments.'
ok(getLabel(labelText)) ok(getLabel(labelText))
}) })

View File

@ -16,7 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import {isHidden, extractSimilarityInfo} from 'jsx/grading/helpers/SubmissionHelper' import {isPostable, extractSimilarityInfo} from 'jsx/grading/helpers/SubmissionHelper'
QUnit.module('SubmissionHelper', suiteHooks => { QUnit.module('SubmissionHelper', suiteHooks => {
let submission let submission
@ -24,19 +24,21 @@ QUnit.module('SubmissionHelper', suiteHooks => {
suiteHooks.beforeEach(() => { suiteHooks.beforeEach(() => {
submission = { submission = {
excused: false, excused: false,
hasPostableComments: false,
score: null, score: null,
submissionComments: [],
workflowState: 'unsubmitted' workflowState: 'unsubmitted'
} }
}) })
QUnit.module('.isHidden', () => { QUnit.module('.isPostable', () => {
QUnit.module('when submission is excused', excusedHooks => { QUnit.module('when submission is excused', excusedHooks => {
excusedHooks.beforeEach(() => { excusedHooks.beforeEach(() => {
submission.excused = true submission.excused = true
}) })
test('returns true', () => { test('returns true', () => {
strictEqual(isHidden(submission), true) strictEqual(isPostable(submission), true)
}) })
}) })
@ -44,17 +46,22 @@ QUnit.module('SubmissionHelper', suiteHooks => {
test('is true when submission workflow state is graded and score is present', () => { test('is true when submission workflow state is graded and score is present', () => {
submission.score = 1 submission.score = 1
submission.workflowState = 'graded' submission.workflowState = 'graded'
strictEqual(isHidden(submission), true) strictEqual(isPostable(submission), true)
}) })
test('is false when workflow state is not graded', () => { test('is true when submission hasPostableComments is true', () => {
submission.hasPostableComments = true
strictEqual(isPostable(submission), true)
})
test('is false when workflow state is not graded and hasPostableComments is not true', () => {
submission.score = 1 submission.score = 1
strictEqual(isHidden(submission), false) strictEqual(isPostable(submission), false)
}) })
test('is false when score is not present', () => { test('is false when score is not present and hasPostableComments is not true', () => {
submission.workflowState = 'graded' submission.workflowState = 'graded'
strictEqual(isHidden(submission), false) strictEqual(isPostable(submission), false)
}) })
}) })
}) })

View File

@ -212,6 +212,7 @@ QUnit.module('SpeedGrader PostPolicies', suiteHooks => {
const submission = { const submission = {
id: '93', id: '93',
assignment_id: '2301', assignment_id: '2301',
has_postable_comments: true,
posted_at: new Date().toISOString(), posted_at: new Date().toISOString(),
score: 1.0, score: 1.0,
user_id: '441', user_id: '441',
@ -220,7 +221,12 @@ QUnit.module('SpeedGrader PostPolicies', suiteHooks => {
postPolicies.showPostAssignmentGradesTray({submissions: [submission]}) postPolicies.showPostAssignmentGradesTray({submissions: [submission]})
const {submissions} = postGradesShowArgs() const {submissions} = postGradesShowArgs()
deepEqual(submissions, [ deepEqual(submissions, [
{postedAt: submission.posted_at, score: 1.0, workflowState: submission.workflow_state} {
hasPostableComments: true,
postedAt: submission.posted_at,
score: 1.0,
workflowState: submission.workflow_state
}
]) ])
}) })

View File

@ -35,9 +35,9 @@ QUnit.module('SpeedGraderPostGradesMenu', hooks => {
function renderAndOpenMenu(customProps) { function renderAndOpenMenu(customProps) {
const props = { const props = {
allowHidingGrades: true, allowHidingGradesOrComments: true,
allowPostingGrades: true, allowPostingGradesOrComments: true,
hasGrades: true, hasGradesOrPostableComments: true,
onHideGrades: () => {}, onHideGrades: () => {},
onPostGrades: () => {}, onPostGrades: () => {},
...customProps ...customProps
@ -65,76 +65,86 @@ QUnit.module('SpeedGraderPostGradesMenu', hooks => {
} }
QUnit.module('menu trigger', () => { QUnit.module('menu trigger', () => {
test('is rendered as an "off" icon when allowPostingGrades is true', () => { test('is rendered as an "off" icon when allowPostingGradesOrComments is true', () => {
renderAndOpenMenu({allowPostingGrades: true}) renderAndOpenMenu({allowPostingGradesOrComments: true})
ok(getMenuTrigger().querySelector('svg[name="IconOff"]')) ok(getMenuTrigger().querySelector('svg[name="IconOff"]'))
}) })
test('is rendered as an "eye" icon when allowPostingGrades is false', () => { test('is rendered as an "eye" icon when allowPostingGradesOrComments is false', () => {
renderAndOpenMenu({allowPostingGrades: false}) renderAndOpenMenu({allowPostingGradesOrComments: false})
ok(getMenuTrigger().querySelector('svg[name="IconEye"]')) ok(getMenuTrigger().querySelector('svg[name="IconEye"]'))
}) })
}) })
QUnit.module('"Post Grades" menu item', () => { QUnit.module('"Post Grades" menu item', () => {
QUnit.module('when allowPostingGrades is true', itemHooks => { QUnit.module('when allowPostingGradesOrComments is true', itemHooks => {
let postGradesSpy let postGradesSpy
itemHooks.beforeEach(() => { itemHooks.beforeEach(() => {
postGradesSpy = sinon.spy() postGradesSpy = sinon.spy()
renderAndOpenMenu({allowPostingGrades: true, onPostGrades: postGradesSpy}) renderAndOpenMenu({allowPostingGradesOrComments: true, onPostGrades: postGradesSpy})
})
test('enables the "Post Grades" menu item', () => {
notOk(getPostGradesMenuItem().getAttribute('aria-disabled'))
}) })
test('retains the text "Post Grades"', () => { test('retains the text "Post Grades"', () => {
strictEqual(getPostGradesMenuItem().textContent, 'Post Grades') strictEqual(getPostGradesMenuItem().textContent, 'Post Grades')
}) })
test('enables the "Post Grades" menu item', () => {
notOk(getPostGradesMenuItem().getAttribute('aria-disabled'))
})
test('fires the onPostGrades event when clicked', () => { test('fires the onPostGrades event when clicked', () => {
getPostGradesMenuItem().click() getPostGradesMenuItem().click()
strictEqual(postGradesSpy.callCount, 1) strictEqual(postGradesSpy.callCount, 1)
}) })
}) })
QUnit.module('when allowPostingGrades is false', itemHooks => { QUnit.module('when allowPostingGradesOrComments is false', contextHooks => {
itemHooks.beforeEach(() => { let context
renderAndOpenMenu({allowPostingGrades: false})
contextHooks.beforeEach(() => {
context = {allowPostingGradesOrComments: false}
}) })
test('disables the "Post Grades" menu item', () => { QUnit.module('when hasGradesOrPostableComments is false', itemHooks => {
strictEqual(getPostGradesMenuItem().getAttribute('aria-disabled'), 'true') itemHooks.beforeEach(() => {
context.hasGradesOrPostableComments = false
renderAndOpenMenu(context)
})
test('sets the text to "No Grades to Post"', () => {
strictEqual(getPostGradesMenuItem().textContent, 'No Grades to Post')
})
test('disables the "No Grades to Post" menu item', () => {
strictEqual(getPostGradesMenuItem().getAttribute('aria-disabled'), 'true')
})
}) })
test('sets the text to "All Grades Posted"', () => { QUnit.module('when hasGradesOrPostableComments is true', itemHooks => {
strictEqual(getPostGradesMenuItem().textContent, 'All Grades Posted') itemHooks.beforeEach(() => {
}) context.hasGradesOrPostableComments = true
}) renderAndOpenMenu(context)
})
QUnit.module('when hasGrades is false', itemHooks => { test('sets the text to "All Grades Posted"', () => {
itemHooks.beforeEach(() => { strictEqual(getPostGradesMenuItem().textContent, 'All Grades Posted')
renderAndOpenMenu({hasGrades: false}) })
})
test('disables the "Post Grades" menu item', () => { test('disables the "All Grades Posted" menu item', () => {
strictEqual(getPostGradesMenuItem().getAttribute('aria-disabled'), 'true') strictEqual(getPostGradesMenuItem().getAttribute('aria-disabled'), 'true')
}) })
test('sets the text to "No Grades to Post"', () => {
strictEqual(getPostGradesMenuItem().textContent, 'No Grades to Post')
}) })
}) })
}) })
QUnit.module('"Hide Grades" menu item', () => { QUnit.module('"Hide Grades" menu item', () => {
QUnit.module('when allowHidingGrades is true', itemHooks => { QUnit.module('when allowHidingGradesOrComments is true', itemHooks => {
let hideGradesSpy let hideGradesSpy
itemHooks.beforeEach(() => { itemHooks.beforeEach(() => {
hideGradesSpy = sinon.spy() hideGradesSpy = sinon.spy()
renderAndOpenMenu({allowHidingGrades: true, onHideGrades: hideGradesSpy}) renderAndOpenMenu({allowHidingGradesOrComments: true, onHideGrades: hideGradesSpy})
}) })
test('enables the "Hide Grades" menu item', () => { test('enables the "Hide Grades" menu item', () => {
@ -151,31 +161,41 @@ QUnit.module('SpeedGraderPostGradesMenu', hooks => {
}) })
}) })
QUnit.module('when allowHidingGrades is false', itemHooks => { QUnit.module('when allowHidingGradesOrComments is false', contextHooks => {
itemHooks.beforeEach(() => { let context
renderAndOpenMenu({allowHidingGrades: false})
contextHooks.beforeEach(() => {
context = {allowHidingGradesOrComments: false}
}) })
test('disables the "Hide Grades" menu item', () => { QUnit.module('when hasGradesOrPostableComments is false', itemHooks => {
strictEqual(getHideGradesMenuItem().getAttribute('aria-disabled'), 'true') itemHooks.beforeEach(() => {
context.hasGradesOrPostableComments = false
renderAndOpenMenu(context)
})
test('sets the text to "No Grades to Hide"', () => {
strictEqual(getHideGradesMenuItem().textContent, 'No Grades to Hide')
})
test('disables the "No Grades to Hide" menu item', () => {
strictEqual(getHideGradesMenuItem().getAttribute('aria-disabled'), 'true')
})
}) })
test('sets the text to "All Grades Hidden"', () => { QUnit.module('when hasGradesOrPostableComments is true', itemHooks => {
strictEqual(getHideGradesMenuItem().textContent, 'All Grades Hidden') itemHooks.beforeEach(() => {
}) context.hasGradesOrPostableComments = true
}) renderAndOpenMenu(context)
})
QUnit.module('when hasGrades is false', itemHooks => { test('sets the text to "All Grades Hidden"', () => {
itemHooks.beforeEach(() => { strictEqual(getHideGradesMenuItem().textContent, 'All Grades Hidden')
renderAndOpenMenu({hasGrades: false}) })
})
test('disables the "Hide Grades" menu item', () => { test('disables the "All Grades Hidden" menu item', () => {
strictEqual(getHideGradesMenuItem().getAttribute('aria-disabled'), 'true') strictEqual(getHideGradesMenuItem().getAttribute('aria-disabled'), 'true')
}) })
test('sets the text to "No Grades to Hide"', () => {
strictEqual(getHideGradesMenuItem().textContent, 'No Grades to Hide')
}) })
}) })
}) })

View File

@ -3789,50 +3789,51 @@ QUnit.module('SpeedGrader', rootHooks => {
}) })
}) })
test('passes the allowHidingGrades prop as true if any submissions are posted', () => { test('passes the allowHidingGradesOrComments prop as true if any submissions are posted', () => {
SpeedGrader.EG.jsonReady() SpeedGrader.EG.jsonReady()
const [SpeedGraderPostGradesMenu] = findRenderCall() const [SpeedGraderPostGradesMenu] = findRenderCall()
strictEqual(SpeedGraderPostGradesMenu.props.allowHidingGrades, true) strictEqual(SpeedGraderPostGradesMenu.props.allowHidingGradesOrComments, true)
}) })
test('passes the allowHidingGrades prop as false if no submissions are posted', () => { test('passes the allowHidingGradesOrComments prop as false if no submissions are posted', () => {
alphaSubmission.posted_at = null alphaSubmission.posted_at = null
omegaSubmission.posted_at = null omegaSubmission.posted_at = null
SpeedGrader.EG.jsonReady() SpeedGrader.EG.jsonReady()
const [SpeedGraderPostGradesMenu] = findRenderCall() const [SpeedGraderPostGradesMenu] = findRenderCall()
strictEqual(SpeedGraderPostGradesMenu.props.allowHidingGrades, false) strictEqual(SpeedGraderPostGradesMenu.props.allowHidingGradesOrComments, false)
}) })
test('passes the allowPostingGrades prop as true if any submissions are unposted', () => { test('passes the allowPostingGradesOrComments prop as true if any submissions are postable', () => {
alphaSubmission.posted_at = null alphaSubmission.posted_at = null
alphaSubmission.has_postable_comments = true
SpeedGrader.EG.jsonReady() SpeedGrader.EG.jsonReady()
const [SpeedGraderPostGradesMenu] = findRenderCall() const [SpeedGraderPostGradesMenu] = findRenderCall()
strictEqual(SpeedGraderPostGradesMenu.props.allowPostingGrades, true) strictEqual(SpeedGraderPostGradesMenu.props.allowPostingGradesOrComments, true)
}) })
test('passes the allowPostingGrades prop as false if all submissions are posted', () => { test('passes the allowPostingGradesOrComments prop as false if all submissions are posted', () => {
SpeedGrader.EG.jsonReady() SpeedGrader.EG.jsonReady()
const [SpeedGraderPostGradesMenu] = findRenderCall() const [SpeedGraderPostGradesMenu] = findRenderCall()
strictEqual(SpeedGraderPostGradesMenu.props.allowPostingGrades, false) strictEqual(SpeedGraderPostGradesMenu.props.allowPostingGradesOrComments, false)
}) })
test('passes the hasGrades prop as true if any submissions are graded', () => { test('passes the hasGradesOrPostableComments prop as true if any submissions are graded', () => {
SpeedGrader.EG.jsonReady() SpeedGrader.EG.jsonReady()
const [SpeedGraderPostGradesMenu] = findRenderCall() const [SpeedGraderPostGradesMenu] = findRenderCall()
strictEqual(SpeedGraderPostGradesMenu.props.hasGrades, true) strictEqual(SpeedGraderPostGradesMenu.props.hasGradesOrPostableComments, true)
}) })
test('passes the hasGrades prop as false if no submissions are graded', () => { test('passes the hasGradesOrPostableComments prop as false if no submissions are graded', () => {
alphaSubmission.score = null alphaSubmission.score = null
SpeedGrader.EG.jsonReady() SpeedGrader.EG.jsonReady()
const [SpeedGraderPostGradesMenu] = findRenderCall() const [SpeedGraderPostGradesMenu] = findRenderCall()
strictEqual(SpeedGraderPostGradesMenu.props.hasGrades, false) strictEqual(SpeedGraderPostGradesMenu.props.hasGradesOrPostableComments, false)
}) })
}) })

View File

@ -280,6 +280,52 @@ describe SpeedGrader::Assignment do
allow(Canvadoc).to receive(:mime_types).and_return("image/png") allow(Canvadoc).to receive(:mime_types).and_return("image/png")
end end
describe "has_postable_comments" do
before(:each) do
PostPolicy.enable_feature!
@course.root_account.enable_feature!(:allow_postable_submission_comments)
@course.enable_feature!(:new_gradebook)
@assignment.ensure_post_policy(post_manually: true)
end
it "is not included when allow_postable_submission_comments feature is not enabled" do
@course.root_account.disable_feature!(:allow_postable_submission_comments)
json = SpeedGrader::Assignment.new(@assignment, @teacher).json
expect(json[:submissions].first).not_to have_key "has_postable_comments"
end
it "is not included when Post Policies are not enabled" do
@course.disable_feature!(:new_gradebook)
json = SpeedGrader::Assignment.new(@assignment, @teacher).json
expect(json[:submissions].first).not_to have_key "has_postable_comments"
end
it "is true when unposted, hidden comments exist, and postable comments feature is enabled" do
student1_sub = @assignment.submissions.find_by!(user: @student_1)
student1_sub.add_comment(author: @teacher, comment: "good job!", hidden: true)
json = SpeedGrader::Assignment.new(@assignment, @teacher).json
submission_json = json[:submissions].find { |sub| sub["user_id"] == student1_sub.user_id.to_s }
expect(submission_json["has_postable_comments"]).to be true
end
it "is not present when unposted, hidden comments exist, and postable comments feature is not enabled" do
@course.root_account.disable_feature!(:allow_postable_submission_comments)
student1_sub = @assignment.submissions.find_by!(user: @student_1)
student1_sub.add_comment(author: @teacher, comment: "good job!", hidden: true)
json = SpeedGrader::Assignment.new(@assignment, @teacher).json
submission_json = json[:submissions].find { |sub| sub["user_id"] == student1_sub.user_id.to_s }
expect(submission_json).not_to have_key "has_postable_comments"
end
it "is false when unposted and only non-hidden comments exist" do
student1_sub = @assignment.submissions.find_by!(user: @student_1)
student1_sub.add_comment(author: @student1, comment: "good job!", hidden: false)
json = SpeedGrader::Assignment.new(@assignment, @teacher).json
submission_json = json[:submissions].find { |sub| sub["user_id"] == student1_sub.user_id.to_s }
expect(submission_json["has_postable_comments"]).to be false
end
end
it "returns submission lateness" do it "returns submission lateness" do
json = SpeedGrader::Assignment.new(@assignment, @teacher).json json = SpeedGrader::Assignment.new(@assignment, @teacher).json
json[:submissions].each do |submission| json[:submissions].each do |submission|

View File

@ -3256,6 +3256,50 @@ describe Submission do
end end
end end
describe "scope: postable" do
subject(:submissions) { assignment.submissions.postable }
let(:assignment) { @course.assignments.create! }
let(:submission) { assignment.submissions.find_by(user: @student) }
it "does not include submissions that neither have grades nor hidden comments" do
submission.add_comment(author: @teacher, comment: "good job!", hidden: false)
is_expected.not_to include(submission)
end
it "includes submissions with hidden comments" do
submission.add_comment(author: @teacher, comment: "good job!", hidden: true)
is_expected.to include(submission)
end
it "includes submissions with a grade" do
assignment.grade_student(@student, grader: @teacher, grade: 10)
is_expected.to include(submission)
end
it "includes submissions that are excused" do
assignment.grade_student(@student, grader: @teacher, excused: true)
is_expected.to include(submission)
end
end
describe "scope: with_hidden_comments" do
subject(:submissions) { assignment.submissions.with_hidden_comments }
let(:assignment) { @course.assignments.create! }
let(:submission) { assignment.submissions.find_by(user: @student) }
it "does not include submissions without a hidden comment" do
submission.add_comment(author: @teacher, comment: "good job!", hidden: false)
is_expected.not_to include(submission)
end
it "includes submissions with hidden comments" do
submission.add_comment(author: @teacher, comment: "good job!", hidden: true)
is_expected.to include(submission)
end
end
describe 'scope: anonymized' do describe 'scope: anonymized' do
subject(:submissions) { assignment.all_submissions.anonymized } subject(:submissions) { assignment.all_submissions.anonymized }