Update Progress Widget UI

Fixes: GOOF-701

Note:
- We are no longer using the `workflow_state` attribute form the
  Attachment model. We are using AttachmentUploadStatus that is
  joined to the Attachment model to retrieve the same results.
  We are now using Redis to track the state of the Attachment
  per Submission which will allow us to give a more accurate view
  of the Attachment state as it is being processed.

Test plan:
- Compile assets for canvas
- Boot up the docker container
- Navigate to a course with the google lti installed
- As a student submit an assignment from google drive
- On the assignment detials, submission details and grade summary
  pages (found on the invision links in the ticket) confirm you
  see the progress widgets and the files correct upload status
- On the submission details page in the grade book confirm you
  can see the icons
- In speedgrader confirm you can see the correct icons on the right
  hand sidebar AND confirm they change accrodignly as you change
  the submission selected from the dropdown if there are multiple
  submissions

Change-Id: I6c1152cb7b450c3c2e3a2ca810233fc222c0967a
Reviewed-on: https://gerrit.instructure.com/180605
Tested-by: Jenkins
Reviewed-by: Joshua Orr <jgorr@instructure.com>
QA-Review: Deepeeca Soundarrajan <dsoundarrajan@instructure.com>
Product-Review: Jesse Poulos <jpoulos@instructure.com>
This commit is contained in:
Nick Houle 2019-02-05 13:19:50 -07:00
parent 59b04ee722
commit 533b02544f
18 changed files with 200 additions and 70 deletions

View File

@ -186,10 +186,10 @@ define [
@submissionIcon: (submission_type, attachments) ->
klass = SubmissionCell.iconFromSubmissionType(submission_type)
if attachments?
workflow_state = attachments[0].workflow_state
if workflow_state == "pending_upload"
upload_status = attachments[0].upload_status
if upload_status == "pending"
klass = "upload"
else if workflow_state == "errored"
else if upload_status == "failed"
klass = "warning"
"<i class='icon-#{htmlEscape klass}' ></i>"

View File

@ -242,6 +242,7 @@ module Lti
def attachment_json(attachment)
attachment_attributes = %w(id display_name filename content-type size created_at updated_at)
attach = filtered_json(model: attachment, whitelist: attachment_attributes)
attach[:upload_status] = AttachmentUploadStatus.upload_status(attachment)
attach[:url] = attachment_url(attachment)
attach
end

View File

@ -26,9 +26,9 @@ import IconWarning from '@instructure/ui-icons/lib/Line/IconWarning'
const presenter = document.querySelectorAll(".assignment_presenter_for_submission")
const progressIcon = (presenterObject) => {
switch (presenterObject.innerText) {
case 'pending_upload':
case 'pending':
return [<IconUpload />, I18n.t("Uploading Submission")]
case 'errored':
case 'failed':
return [<IconWarning />, I18n.t("Submission Failed to Submit")]
default:
return null

View File

@ -241,6 +241,7 @@ module SpeedGrader
json[:attachment][:crocodoc_url] = a.crocodoc_url(@current_user, url_opts)
json[:attachment][:submitted_to_crocodoc] = a.crocodoc_document.present?
json[:attachment][:hijack_crocodoc_session] = a.crocodoc_document.present? && @should_migrate_to_canvadocs
json[:attachment][:upload_status] = AttachmentUploadStatus.upload_status(a)
end
end
end

View File

@ -30,10 +30,12 @@ class GradeSummaryAssignmentPresenter
def upload_status
return unless submission
# The sort here ensures that statuses received are in the failed,
# pending and success order. With that security we can just pluck
# first one.
submission.attachments.
where(workflow_state: ['errored', 'pending_upload']).
order(:workflow_state).
pluck(:workflow_state).
map { |a| AttachmentUploadStatus.upload_status(a) }.
sort.
first
end

View File

@ -72,7 +72,7 @@
</a>
</div>
<% js_bundle 'progress_pill' %>
<span class="assignment_presenter_for_submission" style="display: none;"><%= attachment.workflow_state %></span>
<span class="assignment_presenter_for_submission" style="display: none;"><%= AttachmentUploadStatus.upload_status(attachment) %></span>
<span class="react_pill_container"></span>
<% end %>
<% elsif @current_user_submission.submission_type == "online_quiz" %>

View File

@ -13,10 +13,10 @@
<span class="turnitin_score_container">{{> turnitinScore}}</span>
{{/with}}
{{/if}}
{{#ifEqual workflow_state "pending_upload"}}
{{#ifEqual upload_status "pending"}}
<a href="{{url}}" class="{{mimeClass content-type}}" title="{{filename}}">{{display_name}}</a><i class='icon-upload' title="Uploading" style="padding-left: 5px;"></i>
{{else}}
{{#ifEqual workflow_state "errored" }}
{{#ifEqual upload_status "failed" }}
<a href="{{url}}" class="{{mimeClass content-type}}" title="{{filename}}">{{display_name}}</a><i class='icon-warning' title="Upload Failed" style="padding-left: 5px;"></i>
{{else}}
<a href="{{url}}" class="{{mimeClass content-type}}" title="{{filename}}">{{display_name}}</a>

View File

@ -43,7 +43,8 @@ module Api::V1::Attachment
'folder_id' => attachment.folder_id,
'display_name' => attachment.display_name,
'filename' => attachment.filename,
'workflow_state' => attachment.workflow_state
'workflow_state' => attachment.workflow_state,
'upload_status' => AttachmentUploadStatus.upload_status(attachment)
}
return hash if options[:only] && options[:only].include?('names')

View File

@ -39,8 +39,6 @@ import Tooltip from '@instructure/ui-overlays/lib/components/Tooltip'
import IconUpload from '@instructure/ui-icons/lib/Line/IconUpload'
import IconWarning from '@instructure/ui-icons/lib/Line/IconWarning'
import IconCheckMarkIndeterminate from '@instructure/ui-icons/lib/Line/IconCheckMarkIndeterminate'
import FailedUploadTreeKite from 'jsx/speed_grader/FailedUploadTreeKite'
import WaitingWristWatch from 'jsx/speed_grader/WaitingWristWatch'
import View from '@instructure/ui-layout/lib/components/View'
import Text from '@instructure/ui-elements/lib/components/Text'
import round from 'compiled/util/round'
@ -679,14 +677,14 @@ function unmountCommentTextArea() {
function renderProgressIcon(attachment) {
const mountPoint = document.getElementById('react_pill_container')
let icon = []
switch (attachment.workflow_state) {
case 'pending_upload':
switch (attachment.upload_status) {
case 'pending':
icon = [<IconUpload />, I18n.t('Uploading Submission')]
break
case 'errored':
case 'failed':
icon = [<IconWarning />, I18n.t('Submission Failed to Submit')]
break
case 'processed':
case 'success':
break
default:
icon = [<IconCheckMarkIndeterminate />, I18n.t('No File Submitted')]
@ -2007,7 +2005,7 @@ EG = {
[anonymizableSubmissionIdKey]: submission[anonymizableUserId],
attachmentId: attachment.id,
display_name: attachment.display_name,
attachmentWorkflow: attachment.workflow_state
attachmentWorkflow: attachment.upload_status
},
hrefValues: [anonymizableSubmissionIdKey, 'attachmentId']
})
@ -2280,22 +2278,6 @@ EG = {
}
},
progressSubmissionPreview(attachment) {
if (attachment === undefined) {
return [
<FailedUploadTreeKite />,
I18n.t('Upload Failed'),
I18n.t('Please have the student submit the file again')
]
} else {
return [
<WaitingWristWatch />,
I18n.t('Uploading'),
I18n.t('Canvas is attempting to retreive the submissions. Please check back again later.')
]
}
},
loadSubmissionPreview(attachment, submission) {
clearInterval(sessionTimer)
$submissions_container.children().hide()
@ -2322,38 +2304,11 @@ EG = {
ENV.lti_retrieve_url,
submission.external_tool_url || submission.url
)
} else if (this.canDisplaySpeedGraderImagePreview(jsonData.context, attachment, submission)) {
this.emptyIframeHolder()
const mountPoint = document.getElementById('iframe_holder')
mountPoint.style = ''
const state = this.progressSubmissionPreview(attachment)
ReactDOM.render(
<View margin="large" display="block" as="div" textAlign="center">
{state[0]}
<Text weight="bold" size="large" as="div">
{state[1]}
</Text>
<Text size="medium" as="div">
{state[2]}
</Text>
</View>,
mountPoint
)
} else {
this.renderSubmissionPreview()
}
},
canDisplaySpeedGraderImagePreview(context, attachment, submission) {
const type = submission.submission_type
return (
!context.quiz &&
(type !== 'online_text_entry' && type !== 'media_recording' && type !== 'online_url') &&
attachment === undefined &&
(submission !== undefined || attachment.workflow_state === 'pending_upload')
)
},
emptyIframeHolder(elem) {
elem = elem || $iframe_holder
elem.empty()

View File

@ -49,6 +49,7 @@ shared_examples_for "file uploads api" do
'display_name' => attachment.display_name,
'filename' => attachment.filename,
'workflow_state' => "processed",
'upload_status' => "success",
'size' => attachment.size,
'unlock_at' => attachment.unlock_at ? attachment.unlock_at.as_json : nil,
'locked' => !!attachment.locked,
@ -124,6 +125,7 @@ shared_examples_for "file uploads api" do
'hidden_for_user' => false,
'created_at' => attachment.created_at.as_json,
'updated_at' => attachment.updated_at.as_json,
'upload_status' => "success",
'thumbnail_url' => attachment.has_thumbnail? ? thumbnail_image_url(attachment, attachment.uuid, host: 'www.example.com') : nil,
'modified_at' => attachment.modified_at.as_json,
'mime_class' => attachment.mime_class,

View File

@ -144,6 +144,7 @@ module Lti
"filename" => attachment.filename,
"display_name" => attachment.display_name,
"created_at" => now.iso8601,
"upload_status" => "success",
"updated_at" => now.iso8601
}
]
@ -200,6 +201,7 @@ module Lti
"filename" => attachment.filename,
"display_name" => attachment.display_name,
"created_at" => now.iso8601,
"upload_status" => "success",
"updated_at" => now.iso8601
}
]

View File

@ -1003,6 +1003,7 @@ describe ConversationsController, type: :request do
'hidden_for_user' => false,
'created_at' => attachment.created_at.as_json,
'updated_at' => attachment.updated_at.as_json,
'upload_status' => "success",
'modified_at' => attachment.modified_at.as_json,
'thumbnail_url' => thumbnail_image_url(attachment, attachment.uuid, host: 'www.example.com'),
'mime_class' => attachment.mime_class,
@ -1247,6 +1248,7 @@ describe ConversationsController, type: :request do
'hidden_for_user' => false,
'created_at' => attachment.created_at.as_json,
'updated_at' => attachment.updated_at.as_json,
'upload_status' => "success",
'thumbnail_url' => nil,
'modified_at' => attachment.modified_at.as_json,
'mime_class' => attachment.mime_class,

View File

@ -391,6 +391,7 @@ describe DiscussionTopicsController, type: :request do
'hidden_for_user' => false,
'created_at' => @attachment.created_at.as_json,
'updated_at' => @attachment.updated_at.as_json,
'upload_status' => "success",
'modified_at' => @attachment.modified_at.as_json,
'thumbnail_url' => nil,
'mime_class' => @attachment.mime_class,
@ -1518,6 +1519,7 @@ describe DiscussionTopicsController, type: :request do
'hidden_for_user' => false,
'created_at' => attachment.created_at.as_json,
'updated_at' => attachment.updated_at.as_json,
'upload_status' => "success",
'thumbnail_url' => nil,
'modified_at' => attachment.modified_at.as_json,
'mime_class' => attachment.mime_class,
@ -2598,6 +2600,7 @@ describe DiscussionTopicsController, type: :request do
'hidden_for_user' => false,
'created_at' => @attachment.created_at.as_json,
'updated_at' => @attachment.updated_at.as_json,
'upload_status' => "success",
'thumbnail_url' => nil,
'modified_at' => @attachment.modified_at.as_json,
'mime_class' => @attachment.mime_class,

View File

@ -162,6 +162,7 @@ describe "Files API", type: :request do
'hidden_for_user' => false,
'created_at' => @attachment.created_at.as_json,
'updated_at' => @attachment.updated_at.as_json,
'upload_status' => "success",
'thumbnail_url' => nil,
'modified_at' => @attachment.modified_at.as_json,
'mime_class' => @attachment.mime_class,
@ -201,6 +202,7 @@ describe "Files API", type: :request do
'hidden_for_user' => false,
'created_at' => @attachment.created_at.as_json,
'updated_at' => @attachment.updated_at.as_json,
'upload_status' => "success",
'thumbnail_url' => nil,
'modified_at' => @attachment.modified_at.as_json,
'mime_class' => @attachment.mime_class,
@ -832,6 +834,7 @@ describe "Files API", type: :request do
'hidden_for_user' => false,
'created_at' => @att.created_at.as_json,
'updated_at' => @att.updated_at.as_json,
'upload_status' => "success",
'thumbnail_url' => thumbnail_image_url(@att, @att.uuid, host: 'www.example.com'),
'modified_at' => @att.modified_at.as_json,
'mime_class' => @att.mime_class,

View File

@ -1095,6 +1095,7 @@ describe 'Submissions API', type: :request do
"filename" => "unknown.loser",
"display_name" => "unknown.loser",
"workflow_state" => "pending_upload",
"upload_status" => "success",
"id" => sub1.attachments.first.id,
"uuid" => sub1.attachments.first.uuid,
"folder_id" => sub1.attachments.first.folder_id,
@ -1192,6 +1193,7 @@ describe 'Submissions API', type: :request do
"filename" => "unknown.loser",
"display_name" => "unknown.loser",
"workflow_state" => "pending_upload",
"upload_status" => "success",
"id" => sub1.attachments.first.id,
"uuid" => sub1.attachments.first.uuid,
"folder_id" => sub1.attachments.first.folder_id,
@ -1304,6 +1306,7 @@ describe 'Submissions API', type: :request do
"display_name" => "snapshot.png",
"filename" => "snapshot.png",
"workflow_state" => "pending_upload",
"upload_status" => "success",
"url" => "http://www.example.com/files/#{sub2a1.id}/download?download_frd=1&verifier=#{sub2a1.uuid}",
"id" => sub2a1.id,
"uuid" => sub2a1.uuid,
@ -1342,6 +1345,7 @@ describe 'Submissions API', type: :request do
"display_name" => "snapshot.png",
"filename" => "snapshot.png",
"workflow_state" => "pending_upload",
"upload_status" => "success",
"url" => "http://www.example.com/files/#{sub2a1.id}/download?download_frd=1&verifier=#{sub2a1.uuid}",
"id" => sub2a1.id,
"uuid" => sub2a1.uuid,

View File

@ -554,6 +554,28 @@ test('#loadValue sets the value to grade when entered_grade is not available', f
strictEqual(this.cell.val, 'complete')
})
QUnit.module('SubmissionCell#submissionIcon', () => {
test('returns icon class for an attachment with upload_status of pending', () => {
const attachments = [{upload_status: 'pending'}]
strictEqual(
SubmissionCell.submissionIcon('default', attachments),
"<i class='icon-upload' ></i>"
)
})
test('returns icon class for an attachment with upload_status of failed', () => {
const attachments = [{upload_status: 'failed'}]
strictEqual(
SubmissionCell.submissionIcon('default', attachments),
"<i class='icon-warning' ></i>"
)
})
test('returns icon class of document for an attachment with no attachments', () => {
strictEqual(SubmissionCell.submissionIcon('document'), "<i class='icon-document' ></i>")
})
})
QUnit.module('SubmissionCell#classesBasedOnSubmission', () => {
test('returns anonymous when anonymize_students is set on the assignment', () => {
const assignment = {anonymize_students: true}

View File

@ -2187,6 +2187,135 @@ QUnit.module('SpeedGrader', function(suiteHooks) {
})
})
QUnit.module('#renderProgressIcon', function(hooks) {
const assignment = {}
const student = {
id: '1',
submission_history: []
}
const enrollment = {user_id: student.id, course_section_id: '1'}
const submissionComment = {
created_at: new Date().toISOString(),
publishable: false,
comment: 'a comment',
author_id: 1,
author_name: 'an author'
}
const submission = {
id: '3',
user_id: '1',
grade_matches_current_submission: true,
workflow_state: 'active',
submitted_at: new Date().toISOString(),
grade: 'A',
assignment_id: '456',
submission_comments: [submissionComment]
}
const windowJsonData = {
...assignment,
context_id: '123',
context: {
students: [student],
enrollments: [enrollment],
active_course_sections: [],
rep_for_student: {}
},
submissions: [submission],
gradingPeriods: []
}
let jsonData
let commentToRender
const commentBlankHtml = `
<div class="comment">
<span class="comment"></span>
<button class="submit_comment_button">
<span>Submit</span>
</button>
<a class="delete_comment_link icon-x">
<span class="screenreader-only">Delete comment</span>
</a>
<div class="comment_attachments"></div>
</div>
`
const commentAttachmentBlank = `
<div class="comment_attachment">
<a href="example.com/{{ submitter_id }}/{{ id }}/{{ comment_id }}"><span class="display_name">&nbsp;</span></a>
</div>
`
hooks.beforeEach(() => {
;({jsonData} = window)
fakeENV.setup({
...ENV,
assignment_id: '17',
course_id: '29',
grading_role: 'moderator',
help_url: 'example.com/support',
show_help_menu_item: false,
current_user_id: '1',
RUBRIC_ASSESSMENT: {}
})
setupFixtures(`
<div id="react_pill_container"></div>
`)
SpeedGrader.setup()
window.jsonData = windowJsonData
SpeedGrader.EG.jsonReady()
setupCurrentStudent()
commentToRender = {...submissionComment}
commentToRender.draft = true
commentRenderingOptions = {
commentBlank: $(commentBlankHtml),
commentAttachmentBlank: $(commentAttachmentBlank)
}
})
hooks.afterEach(() => {
teardownFixtures()
delete SpeedGrader.EG.currentStudent
window.jsonData = jsonData
SpeedGrader.teardown()
document.querySelector('.ui-selectmenu-menu').remove()
})
test('mounts the progressIcon when attachment uplod_status is pending', function() {
const attachment = {content_type: 'application/rtf', upload_status: 'pending'}
SpeedGrader.EG.renderAttachment(attachment)
strictEqual(
document.getElementById('react_pill_container').children.length,
0
)
})
test('mounts the progressIcon when attachment uplod_status is failed', function() {
const attachment = {content_type: 'application/rtf', upload_status: 'failed'}
SpeedGrader.EG.renderAttachment(attachment)
strictEqual(
document.getElementById('react_pill_container').children.length,
0
)
})
test('mounts the file name preview when attachment uplod_status is success', function() {
const attachment = {content_type: 'application/rtf', upload_status: 'success'}
SpeedGrader.EG.renderAttachment(attachment)
strictEqual(
document.getElementById('react_pill_container').children.length,
0
)
})
})
QUnit.module('#renderCommentTextArea', function(hooks) {
hooks.beforeEach(function() {
setupFixtures('<div id="speed_grader_comment_textarea_mount_point"/>')

View File

@ -74,17 +74,17 @@ describe GradeSummaryAssignmentPresenter do
end
describe '#upload_status' do
it 'returns attachment workflow_state when workflow_state is pending_upload' do
expect(presenter.upload_status).to eq('pending_upload')
it 'returns attachment upload_status when upload_status is pending' do
allow(Rails.cache).to receive(:read).and_return('pending')
expect(presenter.upload_status).to eq('pending')
end
it 'returns attachment workflow_state when workflow_state is errored' do
@attachment.workflow_state = 'errored'
@attachment.save!
expect(presenter.upload_status).to eq('errored')
it 'returns attachment upload_status when upload_status is failed' do
allow(Rails.cache).to receive(:read).and_return('failed')
expect(presenter.upload_status).to eq('failed')
end
it 'returns the proper attachment when there are muliptle attachments in different states' do
it 'returns the proper attachment when there are multiple attachments in different states' do
attachment_1 = attachment_model(context: @student)
attachment_1.workflow_state = 'success'
attachment_1.save!
@ -93,7 +93,10 @@ describe GradeSummaryAssignmentPresenter do
attachment_2.save!
attachment_3 = attachment_model(context: @student)
@assignment.submit_homework @student, attachments: [attachment_1, attachment_2, attachment_3]
expect(presenter.upload_status).to eq('errored')
AttachmentUploadStatus.success!(attachment_1)
AttachmentUploadStatus.failed!(attachment_2, 'bad things')
AttachmentUploadStatus.pending!(attachment_3)
expect(presenter.upload_status).to eq('failed')
end
end