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

View File

@ -472,6 +472,10 @@ class SubmissionsApiController < ApplicationController
end
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.
preload(:root_account, :sis_pseudonym, :user => :pseudonyms).
where(:user_id => student_ids).order(:user_id)
@ -483,8 +487,17 @@ class SubmissionsApiController < ApplicationController
if params[:workflow_state].present?
submissions_scope = submissions_scope.where(:workflow_state => params[:workflow_state])
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)
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.not(user_id: input[:skip_student_ids]) if input[:skip_student_ids]
submissions_scope = input[:graded_only] ? assignment.submissions.graded : assignment.submissions
submissions_scope = submissions_scope.joins(user: :enrollments).merge(visible_enrollments)
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)
submission_ids = submissions_scope.pluck(:id)
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)
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)
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 {extractDataTurnitin} from 'compiled/gradezilla/Turnitin'
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'
function getTurnitinState(submission) {
@ -214,7 +214,7 @@ export default class AssignmentCellFormatter {
}
const showUnpostedIndicator =
columnDef.postAssignmentGradesTrayOpenForAssignmentId && isHidden(submission)
columnDef.postAssignmentGradesTrayOpenForAssignmentId && isPostable(submission)
const options = {
classNames: classNamesForAssignmentCell(assignmentData, submissionData),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,7 +44,9 @@ export default function PostTypes({anonymousGrading, defaultValue, disabled, pos
<>
<Text>{I18n.t('Everyone')}</Text>
<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}
@ -56,7 +58,9 @@ export default function PostTypes({anonymousGrading, defaultValue, disabled, pos
<Text>{I18n.t('Graded')}</Text>
<br />
<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>
</>
}

View File

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

View File

@ -24,9 +24,9 @@ export function isGraded(submission) {
return (sub.score != null && sub.workflowState === 'graded') || sub.excused
}
export function isHidden(submission) {
export function isPostable(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

View File

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

View File

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

View File

@ -177,6 +177,10 @@ module SpeedGrader
json.merge! provisional_grade_to_json(provisional_grade)
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(
assignment: assignment,
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 :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.
scope :recently_graded_assignments, lambda { |user_id, date, limit|
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
description: Includes grade values in the admin grade reports.
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
if includes.include?("has_postable_comments")
hash["has_postable_comments"] = submission.submission_comments.select(&:hidden?).present?
end
if includes.include?("submission_comments")
published_comments = submission.comments_for(@current_user).published
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 SpeedGraderPostGradesMenu from 'jsx/speed_grader/SpeedGraderPostGradesMenu'
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 submissionsDropdownTemplate from 'jst/speed_grader/submissions_dropdown'
import speechRecognitionTemplate from 'jst/speed_grader/speech_recognition'
@ -758,7 +758,7 @@ function renderProgressIcon(attachment) {
function renderHiddenSubmissionPill(submission) {
const mountPoint = document.getElementById(SPEED_GRADER_HIDDEN_SUBMISSION_PILL_MOUNT_POINT)
if (isHidden(submission)) {
if (isPostable(submission)) {
ReactDOM.render(
<Pill variant="warning" text={I18n.t('Hidden')} margin="0 0 small" />,
mountPoint
@ -3752,11 +3752,15 @@ function renderPostGradesMenu() {
const {submissionsMap} = window.jsonData
const submissions = window.jsonData.studentsWithSubmissions.map(student => student.submission)
const hasGrades = submissions.some(isGraded)
const allowHidingGrades = submissions.some(
const hasGradesOrPostableComments = submissions.some(
submission => isGraded(submission) || submission.has_postable_comments
)
const allowHidingGradesOrComments = submissions.some(
submission => submission && submission.posted_at != null
)
const allowPostingGrades = submissions.some(submission => submission && isHidden(submission))
const allowPostingGradesOrComments = submissions.some(
submission => submission && isPostable(submission)
)
function onHideGrades() {
EG.postPolicies.showHideAssignmentGradesTray({submissionsMap})
@ -3767,9 +3771,9 @@ function renderPostGradesMenu() {
}
const props = {
allowHidingGrades,
allowPostingGrades,
hasGrades,
allowHidingGradesOrComments,
allowPostingGradesOrComments,
hasGradesOrPostableComments,
onHideGrades,
onPostGrades
}

View File

@ -1689,6 +1689,59 @@ describe 'Submissions API', type: :request do
expect(response).to be_forbidden
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
it 'includes has_originality_report if the submission has an originality_report' do
attachment_model

View File

@ -206,41 +206,62 @@ describe Mutations::PostAssignmentGradesForSections do
end
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_submissions) { assignment.submissions.where(user_id: section1_user_ids) }
before(:each) do
section1_student2 = User.create!
section1.enroll_user(section1_student2, "StudentEnrollment", "active")
@section1_student2 = User.create!
section1.enroll_user(@section1_student2, "StudentEnrollment", "active")
@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)
end
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)
post_submissions_job = Delayed::Job.where(tag: "Assignment#post_submissions").order(:id).last
post_submissions_job.invoke_job
expect(@student1_submission.reload).to be_posted
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
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
expect(@student2_submission.reload).not_to be_posted
end
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)
post_submissions_job = Delayed::Job.where(tag: "Assignment#post_submissions").order(:id).last
post_submissions_job.invoke_job
expect(section1_submissions).to all(be_posted)
end
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)
post_submissions_job = Delayed::Job.where(tag: "Assignment#post_submissions").order(:id).last
post_submissions_job.invoke_job
expect(section1_submissions).to all(be_posted)
end

View File

@ -277,6 +277,7 @@ describe Mutations::PostAssignmentGrades do
before(:each) do
@student1_submission = assignment.submissions.find_by(user: student)
@student2_submission = assignment.submissions.find_by(user: student2)
assignment.ensure_post_policy(post_manually: true)
assignment.grade_student(student, grader: teacher, score: 100)
end
@ -293,6 +294,29 @@ describe Mutations::PostAssignmentGrades do
expect(@student1_submission.reload).to be_posted
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
execute_query(mutation_str(assignment_id: assignment.id, graded_only: true), context)
post_submissions_job.invoke_job

View File

@ -375,10 +375,9 @@ QUnit.module('GradebookGrid AssignmentCellFormatter', suiteHooks => {
strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 1)
})
test('does not display an unposted grade indicator when submission is graded and posted', () => {
submission.workflow_state = 'graded'
submission.posted_at = new Date()
strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 0)
test('displays an unposted grade indicator when a submission comment exists and is unposted', () => {
submission.hasPostableComments = true
strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 1)
})
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)
})
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'
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.score = null
submission.hasPostableComments = false
strictEqual(renderCell().querySelectorAll('.Grid__GradeCell__UnpostedGrade').length, 0)
})
})

View File

@ -83,6 +83,7 @@ QUnit.module('GradebookGrid AssignmentColumnHeaderRenderer', suiteHooks => {
id: '93',
assignment_id: '2301',
excused: false,
hasPostableComments: false,
late_policy_status: null,
posted_at: null,
score: null,
@ -362,34 +363,41 @@ QUnit.module('GradebookGrid AssignmentColumnHeaderRenderer', suiteHooks => {
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.score = 1
gradebook.gotChunkOfStudents([student])
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])
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.score = 1
gradebook.gotChunkOfStudents([student])
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')
gradebook.gotChunkOfStudents([student])
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', () => {
@ -447,31 +455,38 @@ QUnit.module('GradebookGrid AssignmentColumnHeaderRenderer', suiteHooks => {
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.score = 1
gradebook.gotChunkOfStudents([student])
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])
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])
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
gradebook.gotChunkOfStudents([student])
render()
strictEqual(component.props.hideGradesAction.hasGradesToHide, false)
strictEqual(component.props.hideGradesAction.hasGradesOrCommentsToHide, false)
})
test('includes a callback to show the "Hide Assignment Grades" tray', () => {

View File

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

View File

@ -253,6 +253,7 @@ QUnit.module('Gradebook PostPolicies', suiteHooks => {
}
submission = {
assignment_id: '2301',
has_postable_comments: true,
posted_at: new Date().toISOString(),
score: 1.0,
workflow_state: 'graded'
@ -309,7 +310,12 @@ QUnit.module('Gradebook PostPolicies', suiteHooks => {
postPolicies.showPostAssignmentGradesTray({assignmentId: '2301'})
const [{submissions}] = postPolicies._postAssignmentGradesTray.show.lastCall.args
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: {
assignmentId: '1',
excused: false,
hasPostableComments: false,
late: false,
missing: false,
postedAt: null,
@ -176,6 +177,13 @@ QUnit.module('SubmissionStatus - Pills', hooks => {
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', () => {
props.submission.workflowState = 'unsubmitted'
wrapper = mountComponent()
@ -218,6 +226,7 @@ QUnit.module('SubmissionStatus - Grading Period not in any grading period warnin
postPoliciesEnabled: false,
submission: {
excused: false,
hasPostableComments: false,
late: false,
missing: false,
secondsLate: 0,
@ -278,6 +287,7 @@ QUnit.module('SubmissionStatus - Grading Period is a closed warning', hooks => {
postPoliciesEnabled: false,
submission: {
excused: false,
hasPostableComments: false,
late: false,
missing: false,
secondsLate: 0,
@ -338,6 +348,7 @@ QUnit.module('SubmissionStatus - Grading Period is in another period warning', h
postPoliciesEnabled: false,
submission: {
excused: false,
hasPostableComments: false,
late: false,
missing: false,
secondsLate: 0,
@ -398,6 +409,7 @@ QUnit.module('SubmissionStatus - Concluded Enrollment Warning', hooks => {
postPoliciesEnabled: false,
submission: {
excused: false,
hasPostableComments: false,
late: false,
missing: false,
secondsLate: 0,
@ -458,6 +470,7 @@ QUnit.module('SubmissionStatus - Not calculated in final grade', hooks => {
postPoliciesEnabled: false,
submission: {
excused: false,
hasPostableComments: false,
late: false,
missing: false,
secondsLate: 0,

View File

@ -74,7 +74,8 @@ QUnit.module('SubmissionTray', hooks => {
enteredScore: 10,
excused: false,
grade: '7',
gradedAt: null,
gradedAt: new Date().toISOString(),
hasPostableComments: false,
id: '2501',
late: false,
missing: false,
@ -83,7 +84,8 @@ QUnit.module('SubmissionTray', hooks => {
score: 7,
secondsLate: 0,
submissionType: 'online_text_entry',
userId: '27'
userId: '27',
workflowState: 'graded'
},
updateSubmission() {},
updateSubmissionComment() {},
@ -94,6 +96,7 @@ QUnit.module('SubmissionTray', hooks => {
gradingType: 'points',
htmlUrl: 'http://htmlUrl/',
id: '30',
moderatedGrading: false,
muted: false,
pointsPossible: 10,
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', () => {
const avatarUrl = 'http://bob_is_not_a_domain/me.jpg?filter=make_me_pretty'
const gradesUrl = 'http://gradesUrl/'
@ -334,13 +355,6 @@ QUnit.module('SubmissionTray', hooks => {
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', () => {
mountComponent({
student: {id: '27', name: 'Sara', gradesUrl: 'http://gradeUrl/', isConcluded: false}

View File

@ -243,6 +243,16 @@ QUnit.module('PostAssignmentGradesTray', suiteHooks => {
await show()
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 => {

View File

@ -30,12 +30,14 @@ QUnit.module('PostAssignmentGradesTray PostTypes', suiteHooks => {
}
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)
}
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)
}
@ -66,13 +68,15 @@ QUnit.module('PostAssignmentGradesTray PostTypes', suiteHooks => {
test('"Everyone" type includes description"', () => {
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))
})
test('"Graded" type includes description"', () => {
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))
})

View File

@ -16,7 +16,7 @@
* 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 => {
let submission
@ -24,19 +24,21 @@ QUnit.module('SubmissionHelper', suiteHooks => {
suiteHooks.beforeEach(() => {
submission = {
excused: false,
hasPostableComments: false,
score: null,
submissionComments: [],
workflowState: 'unsubmitted'
}
})
QUnit.module('.isHidden', () => {
QUnit.module('.isPostable', () => {
QUnit.module('when submission is excused', excusedHooks => {
excusedHooks.beforeEach(() => {
submission.excused = 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', () => {
submission.score = 1
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
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'
strictEqual(isHidden(submission), false)
strictEqual(isPostable(submission), false)
})
})
})

View File

@ -212,6 +212,7 @@ QUnit.module('SpeedGrader PostPolicies', suiteHooks => {
const submission = {
id: '93',
assignment_id: '2301',
has_postable_comments: true,
posted_at: new Date().toISOString(),
score: 1.0,
user_id: '441',
@ -220,7 +221,12 @@ QUnit.module('SpeedGrader PostPolicies', suiteHooks => {
postPolicies.showPostAssignmentGradesTray({submissions: [submission]})
const {submissions} = postGradesShowArgs()
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) {
const props = {
allowHidingGrades: true,
allowPostingGrades: true,
hasGrades: true,
allowHidingGradesOrComments: true,
allowPostingGradesOrComments: true,
hasGradesOrPostableComments: true,
onHideGrades: () => {},
onPostGrades: () => {},
...customProps
@ -65,76 +65,86 @@ QUnit.module('SpeedGraderPostGradesMenu', hooks => {
}
QUnit.module('menu trigger', () => {
test('is rendered as an "off" icon when allowPostingGrades is true', () => {
renderAndOpenMenu({allowPostingGrades: true})
test('is rendered as an "off" icon when allowPostingGradesOrComments is true', () => {
renderAndOpenMenu({allowPostingGradesOrComments: true})
ok(getMenuTrigger().querySelector('svg[name="IconOff"]'))
})
test('is rendered as an "eye" icon when allowPostingGrades is false', () => {
renderAndOpenMenu({allowPostingGrades: false})
test('is rendered as an "eye" icon when allowPostingGradesOrComments is false', () => {
renderAndOpenMenu({allowPostingGradesOrComments: false})
ok(getMenuTrigger().querySelector('svg[name="IconEye"]'))
})
})
QUnit.module('"Post Grades" menu item', () => {
QUnit.module('when allowPostingGrades is true', itemHooks => {
QUnit.module('when allowPostingGradesOrComments is true', itemHooks => {
let postGradesSpy
itemHooks.beforeEach(() => {
postGradesSpy = sinon.spy()
renderAndOpenMenu({allowPostingGrades: true, onPostGrades: postGradesSpy})
})
test('enables the "Post Grades" menu item', () => {
notOk(getPostGradesMenuItem().getAttribute('aria-disabled'))
renderAndOpenMenu({allowPostingGradesOrComments: true, onPostGrades: postGradesSpy})
})
test('retains the text "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', () => {
getPostGradesMenuItem().click()
strictEqual(postGradesSpy.callCount, 1)
})
})
QUnit.module('when allowPostingGrades is false', itemHooks => {
itemHooks.beforeEach(() => {
renderAndOpenMenu({allowPostingGrades: false})
QUnit.module('when allowPostingGradesOrComments is false', contextHooks => {
let context
contextHooks.beforeEach(() => {
context = {allowPostingGradesOrComments: false}
})
test('disables the "Post Grades" menu item', () => {
strictEqual(getPostGradesMenuItem().getAttribute('aria-disabled'), 'true')
QUnit.module('when hasGradesOrPostableComments is false', itemHooks => {
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"', () => {
strictEqual(getPostGradesMenuItem().textContent, 'All Grades Posted')
})
})
QUnit.module('when hasGradesOrPostableComments is true', itemHooks => {
itemHooks.beforeEach(() => {
context.hasGradesOrPostableComments = true
renderAndOpenMenu(context)
})
QUnit.module('when hasGrades is false', itemHooks => {
itemHooks.beforeEach(() => {
renderAndOpenMenu({hasGrades: false})
})
test('sets the text to "All Grades Posted"', () => {
strictEqual(getPostGradesMenuItem().textContent, 'All Grades Posted')
})
test('disables the "Post Grades" menu item', () => {
strictEqual(getPostGradesMenuItem().getAttribute('aria-disabled'), 'true')
})
test('sets the text to "No Grades to Post"', () => {
strictEqual(getPostGradesMenuItem().textContent, 'No Grades to Post')
test('disables the "All Grades Posted" menu item', () => {
strictEqual(getPostGradesMenuItem().getAttribute('aria-disabled'), 'true')
})
})
})
})
QUnit.module('"Hide Grades" menu item', () => {
QUnit.module('when allowHidingGrades is true', itemHooks => {
QUnit.module('when allowHidingGradesOrComments is true', itemHooks => {
let hideGradesSpy
itemHooks.beforeEach(() => {
hideGradesSpy = sinon.spy()
renderAndOpenMenu({allowHidingGrades: true, onHideGrades: hideGradesSpy})
renderAndOpenMenu({allowHidingGradesOrComments: true, onHideGrades: hideGradesSpy})
})
test('enables the "Hide Grades" menu item', () => {
@ -151,31 +161,41 @@ QUnit.module('SpeedGraderPostGradesMenu', hooks => {
})
})
QUnit.module('when allowHidingGrades is false', itemHooks => {
itemHooks.beforeEach(() => {
renderAndOpenMenu({allowHidingGrades: false})
QUnit.module('when allowHidingGradesOrComments is false', contextHooks => {
let context
contextHooks.beforeEach(() => {
context = {allowHidingGradesOrComments: false}
})
test('disables the "Hide Grades" menu item', () => {
strictEqual(getHideGradesMenuItem().getAttribute('aria-disabled'), 'true')
QUnit.module('when hasGradesOrPostableComments is false', itemHooks => {
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"', () => {
strictEqual(getHideGradesMenuItem().textContent, 'All Grades Hidden')
})
})
QUnit.module('when hasGradesOrPostableComments is true', itemHooks => {
itemHooks.beforeEach(() => {
context.hasGradesOrPostableComments = true
renderAndOpenMenu(context)
})
QUnit.module('when hasGrades is false', itemHooks => {
itemHooks.beforeEach(() => {
renderAndOpenMenu({hasGrades: false})
})
test('sets the text to "All Grades Hidden"', () => {
strictEqual(getHideGradesMenuItem().textContent, 'All Grades Hidden')
})
test('disables the "Hide Grades" menu item', () => {
strictEqual(getHideGradesMenuItem().getAttribute('aria-disabled'), 'true')
})
test('sets the text to "No Grades to Hide"', () => {
strictEqual(getHideGradesMenuItem().textContent, 'No Grades to Hide')
test('disables the "All Grades Hidden" menu item', () => {
strictEqual(getHideGradesMenuItem().getAttribute('aria-disabled'), 'true')
})
})
})
})

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()
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
omegaSubmission.posted_at = null
SpeedGrader.EG.jsonReady()
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.has_postable_comments = true
SpeedGrader.EG.jsonReady()
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()
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()
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
SpeedGrader.EG.jsonReady()
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")
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
json = SpeedGrader::Assignment.new(@assignment, @teacher).json
json[:submissions].each do |submission|

View File

@ -3256,6 +3256,50 @@ describe Submission do
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
subject(:submissions) { assignment.all_submissions.anonymized }